Skip to content

Commit 18f89e5

Browse files
committed
Add lifecycle hooks for Worker as well (start, stop)
As these would be needed for job-iteration to know when the worker is going to stop.
1 parent 7f84cb0 commit 18f89e5

File tree

9 files changed

+139
-94
lines changed

9 files changed

+139
-94
lines changed

README.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,17 +208,33 @@ Finally, run the migrations:
208208
$ bin/rails db:migrate
209209
```
210210

211-
## Supervisor's lifecycle hooks
212-
You can hook into two different points in the supervisor's life in Solid Queue:
211+
## Lifecycle hooks
212+
213+
In Solid queue, you can hook into two different points in the supervisor's life:
213214
- `start`: after the supervisor has finished booting and right before it forks workers and dispatchers.
214215
- `stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown.
215216

216-
To do that, you just need to call `SolidQueue.on_start` and `SolidQueue.on_stop` with a block, like this:
217+
And into two different points in a worker's life:
218+
- `worker_start`: after the worker has finished booting and right before it starts the polling loop.
219+
- `worker_stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown (which is just `exit!`).
220+
221+
You can use the following methods with a block to do this:
222+
```ruby
223+
SolidQueue.on_start
224+
SolidQueue.on_stop
225+
226+
SolidQueue.on_worker_start
227+
SolidQueue.on_worker_stop
228+
```
229+
230+
For example:
217231
```ruby
218232
SolidQueue.on_start { start_metrics_server }
219233
SolidQueue.on_stop { stop_metrics_server }
220234
```
221235

236+
These can be called several times to add multiple hooks, but it needs to happen before Solid Queue is started. An initializer would be a good place to do this.
237+
222238

223239
### Other configuration settings
224240
_Note_: The settings in this section should be set in your `config/application.rb` or your environment config like this: `config.solid_queue.silence_polling = true`

lib/solid_queue.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ module SolidQueue
4545

4646
delegate :on_start, :on_stop, to: Supervisor
4747

48+
def on_worker_start(...)
49+
Worker.on_start(...)
50+
end
51+
52+
def on_worker_stop(...)
53+
Worker.on_stop(...)
54+
end
55+
4856
def supervisor?
4957
supervisor
5058
end

lib/solid_queue/lifecycle_hooks.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# frozen_string_literal: true
2+
3+
module SolidQueue
4+
module LifecycleHooks
5+
extend ActiveSupport::Concern
6+
7+
included do
8+
mattr_reader :lifecycle_hooks, default: { start: [], stop: [] }
9+
end
10+
11+
class_methods do
12+
def on_start(&block)
13+
self.lifecycle_hooks[:start] << block
14+
end
15+
16+
def on_stop(&block)
17+
self.lifecycle_hooks[:stop] << block
18+
end
19+
20+
def clear_hooks
21+
self.lifecycle_hooks[:start] = []
22+
self.lifecycle_hooks[:stop] = []
23+
end
24+
end
25+
26+
private
27+
def run_start_hooks
28+
run_hooks_for :start
29+
end
30+
31+
def run_stop_hooks
32+
run_hooks_for :stop
33+
end
34+
35+
def run_hooks_for(event)
36+
self.class.lifecycle_hooks.fetch(event, []).each do |block|
37+
block.call
38+
rescue Exception => exception
39+
handle_thread_error(exception)
40+
end
41+
end
42+
end
43+
end

lib/solid_queue/processes/runnable.rb

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@ module Runnable
77
attr_writer :mode
88

99
def start
10-
@stopped = false
11-
12-
SolidQueue.instrument(:start_process, process: self) do
13-
run_callbacks(:boot) { boot }
14-
end
10+
boot
1511

1612
if running_async?
1713
@thread = create_thread { run }
@@ -33,9 +29,15 @@ def mode
3329
end
3430

3531
def boot
36-
if running_as_fork?
37-
register_signal_handlers
38-
set_procline
32+
SolidQueue.instrument(:start_process, process: self) do
33+
run_callbacks(:boot) do
34+
@stopped = false
35+
36+
if running_as_fork?
37+
register_signal_handlers
38+
set_procline
39+
end
40+
end
3941
end
4042
end
4143

lib/solid_queue/supervisor.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
module SolidQueue
44
class Supervisor < Processes::Base
5-
include Maintenance, LifecycleHooks, Signals, Pidfiled
5+
include LifecycleHooks
6+
include Maintenance, Signals, Pidfiled
67

78
class << self
89
def start(load_configuration_from: nil)

lib/solid_queue/supervisor/lifecycle_hooks.rb

Lines changed: 0 additions & 45 deletions
This file was deleted.

lib/solid_queue/worker.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
module SolidQueue
44
class Worker < Processes::Poller
5+
include LifecycleHooks
6+
7+
after_boot :run_start_hooks
8+
before_shutdown :run_stop_hooks
9+
510
attr_accessor :queues, :pool
611

712
def initialize(**options)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
5+
class LifecycleHooksTest < ActiveSupport::TestCase
6+
self.use_transactional_tests = false
7+
8+
test "run lifecycle hooks" do
9+
SolidQueue.on_start { JobResult.create!(status: :hook_called, value: :start) }
10+
SolidQueue.on_stop { JobResult.create!(status: :hook_called, value: :stop) }
11+
12+
SolidQueue.on_worker_start { JobResult.create!(status: :hook_called, value: :worker_start) }
13+
SolidQueue.on_worker_stop { JobResult.create!(status: :hook_called, value: :worker_stop) }
14+
15+
pid = run_supervisor_as_fork(load_configuration_from: { workers: [ { queues: "*" } ] })
16+
wait_for_registered_processes(4)
17+
18+
terminate_process(pid)
19+
wait_for_registered_processes(0)
20+
21+
results = skip_active_record_query_cache do
22+
assert_equal 4, JobResult.count
23+
JobResult.last(4)
24+
end
25+
26+
assert_equal "hook_called", results.map(&:status).first
27+
assert_equal [ "start", "stop", "worker_start", "worker_stop" ], results.map(&:value).sort
28+
ensure
29+
SolidQueue::Supervisor.clear_hooks
30+
SolidQueue::Worker.clear_hooks
31+
end
32+
33+
test "handle errors on lifecycle hooks" do
34+
previous_on_thread_error, SolidQueue.on_thread_error = SolidQueue.on_thread_error, ->(error) { JobResult.create!(status: :error, value: error.message) }
35+
SolidQueue.on_start { raise RuntimeError, "everything is broken" }
36+
37+
pid = run_supervisor_as_fork
38+
wait_for_registered_processes(4)
39+
40+
terminate_process(pid)
41+
wait_for_registered_processes(0)
42+
43+
result = skip_active_record_query_cache { JobResult.last }
44+
45+
assert_equal "error", result.status
46+
assert_equal "everything is broken", result.value
47+
ensure
48+
SolidQueue.on_thread_error = previous_on_thread_error
49+
SolidQueue::Supervisor.clear_hooks
50+
SolidQueue::Worker.clear_hooks
51+
end
52+
end

test/unit/supervisor_test.rb

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -128,43 +128,6 @@ class SupervisorTest < ActiveSupport::TestCase
128128
end
129129
end
130130

131-
test "run lifecycle hooks" do
132-
SolidQueue.on_start { JobResult.create!(status: :hook_called, value: :start) }
133-
SolidQueue.on_stop { JobResult.create!(status: :hook_called, value: :stop) }
134-
135-
pid = run_supervisor_as_fork
136-
wait_for_registered_processes(4)
137-
138-
terminate_process(pid)
139-
wait_for_registered_processes(0)
140-
141-
results = skip_active_record_query_cache { JobResult.last(2) }
142-
143-
assert_equal "hook_called", results.map(&:status).first
144-
assert_equal [ "start", "stop" ], results.map(&:value)
145-
ensure
146-
SolidQueue::Supervisor.clear_hooks
147-
end
148-
149-
test "handle errors on lifecycle hooks" do
150-
previous_on_thread_error, SolidQueue.on_thread_error = SolidQueue.on_thread_error, ->(error) { JobResult.create!(status: :error, value: error.message) }
151-
SolidQueue.on_start { raise RuntimeError, "everything is broken" }
152-
153-
pid = run_supervisor_as_fork
154-
wait_for_registered_processes(4)
155-
156-
terminate_process(pid)
157-
wait_for_registered_processes(0)
158-
159-
result = skip_active_record_query_cache { JobResult.last }
160-
161-
assert_equal "error", result.status
162-
assert_equal "everything is broken", result.value
163-
ensure
164-
SolidQueue.on_thread_error = previous_on_thread_error
165-
SolidQueue::Supervisor.clear_hooks
166-
end
167-
168131
private
169132
def assert_registered_workers(supervisor_pid: nil, count: 1)
170133
assert_registered_processes(kind: "Worker", count: count, supervisor_pid: supervisor_pid)

0 commit comments

Comments
 (0)