Skip to content

Commit d1be120

Browse files
committed
Implement simple lifecycle hooks for the supervisor on start and stop
And document those in the README.
1 parent 13a29a0 commit d1be120

File tree

5 files changed

+99
-1
lines changed

5 files changed

+99
-1
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ 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:
213+
- `start`: after the supervisor has finished booting and right before it forks workers and dispatchers.
214+
- `stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown.
215+
216+
To do that, you just need to call `SolidQueue.on_start` and `SolidQueue.on_stop` with a block, like this:
217+
```ruby
218+
SolidQueue.on_start { start_metrics_server }
219+
SolidQueue.on_stop { stop_metrics_server }
220+
```
221+
222+
211223
### Other configuration settings
212224
_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`
213225

lib/solid_queue.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ module SolidQueue
4343
mattr_accessor :clear_finished_jobs_after, default: 1.day
4444
mattr_accessor :default_concurrency_control_period, default: 3.minutes
4545

46+
delegate :on_start, :on_stop, to: Supervisor
47+
4648
def supervisor?
4749
supervisor
4850
end

lib/solid_queue/supervisor.rb

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

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

77
class << self
88
def start(load_configuration_from: nil)
@@ -27,6 +27,7 @@ def initialize(configuration)
2727

2828
def start
2929
boot
30+
run_start_hooks
3031

3132
start_processes
3233
launch_maintenance_task
@@ -36,6 +37,7 @@ def start
3637

3738
def stop
3839
@stopped = true
40+
run_stop_hooks
3941
end
4042

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

test/unit/supervisor_test.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,43 @@ 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+
131168
private
132169
def assert_registered_workers(supervisor_pid: nil, count: 1)
133170
assert_registered_processes(kind: "Worker", count: count, supervisor_pid: supervisor_pid)

0 commit comments

Comments
 (0)