Skip to content

Commit 835ec4d

Browse files
authored
Merge pull request #317 from rails/start-stop-callbacks
Implement `start` and `stop` lifecycle hooks for the supervisor and workers
2 parents 3ef85e1 + 18f89e5 commit 835ec4d

File tree

8 files changed

+154
-13
lines changed

8 files changed

+154
-13
lines changed

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,17 +208,47 @@ Finally, run the migrations:
208208
$ bin/rails db:migrate
209209
```
210210

211+
## Lifecycle hooks
212+
213+
In Solid queue, you can hook into two different points in the supervisor's life:
214+
- `start`: after the supervisor has finished booting and right before it forks workers and dispatchers.
215+
- `stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown.
216+
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:
231+
```ruby
232+
SolidQueue.on_start { start_metrics_server }
233+
SolidQueue.on_stop { stop_metrics_server }
234+
```
235+
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+
238+
211239
### Other configuration settings
212240
_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`
213241

214242
There are several settings that control how Solid Queue works that you can set as well:
215243
- `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
216244
- `app_executor`: the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
217-
- `on_thread_error`: custom lambda/Proc to call when there's an error within a thread that takes the exception raised as argument. Defaults to
245+
- `on_thread_error`: custom lambda/Proc to call when there's an error within a Solid Queue thread that takes the exception raised as argument. Defaults to
218246

219247
```ruby
220248
-> (exception) { Rails.error.report(exception, handled: false) }
221249
```
250+
**This is not used for errors raised within a job execution**. Errors happening in jobs are handled by Active Job's `retry_on` or `discard_on`, and ultimately will result in [failed jobs](#failed-jobs-and-retries). This is for errors happening within Solid Queue itself.
251+
222252
- `use_skip_locked`: whether to use `FOR UPDATE SKIP LOCKED` when performing locking reads. This will be automatically detected in the future, and for now, you'd only need to set this to `false` if your database doesn't support it. For MySQL, that'd be versions < 8, and for PostgreSQL, versions < 9.5. If you use SQLite, this has no effect, as writes are sequential.
223253
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
224254
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.

lib/solid_queue.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ 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+
48+
def on_worker_start(...)
49+
Worker.on_start(...)
50+
end
51+
52+
def on_worker_stop(...)
53+
Worker.on_stop(...)
54+
end
55+
4656
def supervisor?
4757
supervisor
4858
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 & 12 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 }
@@ -25,10 +21,6 @@ def stop
2521
@thread&.join
2622
end
2723

28-
def alive?
29-
!running_async? || @thread.alive?
30-
end
31-
3224
private
3325
DEFAULT_MODE = :async
3426

@@ -37,9 +29,15 @@ def mode
3729
end
3830

3931
def boot
40-
if running_as_fork?
41-
register_signal_handlers
42-
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
4341
end
4442
end
4543

lib/solid_queue/supervisor.rb

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

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

78
class << self
@@ -27,6 +28,7 @@ def initialize(configuration)
2728

2829
def start
2930
boot
31+
run_start_hooks
3032

3133
start_processes
3234
launch_maintenance_task
@@ -36,6 +38,7 @@ def start
3638

3739
def stop
3840
@stopped = true
41+
run_stop_hooks
3942
end
4043

4144
private

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
File renamed without changes.

0 commit comments

Comments
 (0)