solid_queue 1.2.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +43 -16
- data/lib/puma/plugin/solid_queue.rb +67 -21
- data/lib/solid_queue/app_executor.rb +10 -0
- data/lib/solid_queue/async_supervisor.rb +50 -0
- data/lib/solid_queue/cli.rb +4 -0
- data/lib/solid_queue/configuration.rb +16 -1
- data/lib/solid_queue/dispatcher.rb +1 -0
- data/lib/solid_queue/fork_supervisor.rb +68 -0
- data/lib/solid_queue/processes/runnable.rb +25 -18
- data/lib/solid_queue/processes/thread_terminated_error.rb +11 -0
- data/lib/solid_queue/supervisor/maintenance.rb +11 -0
- data/lib/solid_queue/supervisor/signals.rb +2 -2
- data/lib/solid_queue/supervisor.rb +40 -84
- data/lib/solid_queue/timer.rb +3 -3
- data/lib/solid_queue/version.rb +1 -1
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae346e5b4e8249ab21441c1b45118745124d1d7d78b5b43c08cf6b333e0ec665
|
|
4
|
+
data.tar.gz: fc4b4a6131ff3cb5799f1325f9b0523606152bf8e331bf7122af4463c543e2b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2282d35fe14f5dfa424f1016dd8e00b2f3613e7023cb48cf99b82de5e2079de0154e7102fef72867060cdf4f1e7c9aa125cbed43c229ead9c200342c74e41099
|
|
7
|
+
data.tar.gz: cab19aa7d9d4a22fc3c068ebe00d2bbd1c288c023a37ef24b461bc8d84d9c3e100337ffcd53bf634d17e712e42937175e59cc6b2041e81661e91edfa9fb94282
|
data/README.md
CHANGED
|
@@ -14,8 +14,9 @@ Solid Queue can be used with SQL databases such as MySQL, PostgreSQL, or SQLite,
|
|
|
14
14
|
- [Dashboard UI Setup](#dashboard-ui-setup)
|
|
15
15
|
- [Incremental adoption](#incremental-adoption)
|
|
16
16
|
- [High performance requirements](#high-performance-requirements)
|
|
17
|
+
- [Workers, dispatchers, and scheduler](#workers-dispatchers-and-scheduler)
|
|
18
|
+
- [Fork vs. async mode](#fork-vs-async-mode)
|
|
17
19
|
- [Configuration](#configuration)
|
|
18
|
-
- [Workers, dispatchers, and scheduler](#workers-dispatchers-and-scheduler)
|
|
19
20
|
- [Queue order and priorities](#queue-order-and-priorities)
|
|
20
21
|
- [Queues specification and performance](#queues-specification-and-performance)
|
|
21
22
|
- [Threads, processes, and signals](#threads-processes-and-signals)
|
|
@@ -92,10 +93,10 @@ development:
|
|
|
92
93
|
+ primary:
|
|
93
94
|
<<: *default
|
|
94
95
|
database: storage/development.sqlite3
|
|
95
|
-
+
|
|
96
|
-
+
|
|
97
|
-
+
|
|
98
|
-
+
|
|
96
|
+
+ queue:
|
|
97
|
+
+ <<: *default
|
|
98
|
+
+ database: storage/development_queue.sqlite3
|
|
99
|
+
+ migrations_paths: db/queue_migrate
|
|
99
100
|
```
|
|
100
101
|
|
|
101
102
|
Next, add the following to `development.rb`
|
|
@@ -127,7 +128,7 @@ In `config/cable.yml`
|
|
|
127
128
|
```diff
|
|
128
129
|
development:
|
|
129
130
|
- adapter: async
|
|
130
|
-
+
|
|
131
|
+
+ adapter: solid_cable
|
|
131
132
|
+ connects_to:
|
|
132
133
|
+ database:
|
|
133
134
|
+ writing: cable
|
|
@@ -177,11 +178,9 @@ end
|
|
|
177
178
|
|
|
178
179
|
### High performance requirements
|
|
179
180
|
|
|
180
|
-
Solid Queue was designed for the highest throughput when used with MySQL 8
|
|
181
|
-
|
|
182
|
-
## Configuration
|
|
181
|
+
Solid Queue was designed for the highest throughput when used with MySQL 8+, MariaDB 10.6+, or PostgreSQL 9.5+, as they support `FOR UPDATE SKIP LOCKED`. You can use it with older versions, but in that case, you might run into lock waits if you run multiple workers for the same queue. You can also use it with SQLite on smaller applications.
|
|
183
182
|
|
|
184
|
-
|
|
183
|
+
## Workers, dispatchers, and scheduler
|
|
185
184
|
|
|
186
185
|
We have several types of actors in Solid Queue:
|
|
187
186
|
|
|
@@ -190,7 +189,19 @@ We have several types of actors in Solid Queue:
|
|
|
190
189
|
- The _scheduler_ manages [recurring tasks](#recurring-tasks), enqueuing jobs for them when they're due.
|
|
191
190
|
- The _supervisor_ runs workers and dispatchers according to the configuration, controls their heartbeats, and stops and starts them when needed.
|
|
192
191
|
|
|
193
|
-
|
|
192
|
+
### Fork vs. async mode
|
|
193
|
+
|
|
194
|
+
By default, Solid Queue runs in `fork` mode. This means the supervisor will fork a separate process for each supervised worker/dispatcher/scheduler. This provides the best isolation and performance, but can have additional memory usage and might not work with some Ruby implementations. As an alternative, you can run all workers, dispatchers and schedulers in the same process as the supervisor, in different threads, with an `async` mode. You can choose this mode by running `bin/jobs` as:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
bin/jobs --mode async
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Or you can also set the environment variable `SOLID_QUEUE_SUPERVISOR_MODE` to `async`. If you use the `async` mode, the `processes` option in the configuration described below will be ignored.
|
|
201
|
+
|
|
202
|
+
**The recommended and default mode is `fork`. Only use `async` if you know what you're doing and have strong reasons to**
|
|
203
|
+
|
|
204
|
+
## Configuration
|
|
194
205
|
|
|
195
206
|
By default, Solid Queue will try to find your configuration under `config/queue.yml`, but you can set a different path using the environment variable `SOLID_QUEUE_CONFIG` or by using the `-c/--config_file` option with `bin/jobs`, like this:
|
|
196
207
|
|
|
@@ -254,7 +265,7 @@ Here's an overview of the different options:
|
|
|
254
265
|
|
|
255
266
|
- `threads`: this is the max size of the thread pool that each worker will have to run jobs. Each worker will fetch this number of jobs from their queue(s), at most and will post them to the thread pool to be run. By default, this is `3`. Only workers have this setting.
|
|
256
267
|
It is recommended to set this value less than or equal to the queue database's connection pool size minus 2, as each worker thread uses one connection, and two additional connections are reserved for polling and heartbeat.
|
|
257
|
-
- `processes`: this is the number of worker processes that will be forked by the supervisor with the settings given. By default, this is `1`, just a single process. This setting is useful if you want to dedicate more than one CPU core to a queue or queues with the same configuration. Only workers have this setting.
|
|
268
|
+
- `processes`: this is the number of worker processes that will be forked by the supervisor with the settings given. By default, this is `1`, just a single process. This setting is useful if you want to dedicate more than one CPU core to a queue or queues with the same configuration. Only workers have this setting. **Note**: this option will be ignored if [running in `async` mode](#fork-vs-async-mode).
|
|
258
269
|
- `concurrency_maintenance`: whether the dispatcher will perform the concurrency maintenance work. This is `true` by default, and it's useful if you don't use any [concurrency controls](#concurrency-controls) and want to disable it or if you run multiple dispatchers and want some of them to just dispatch jobs without doing anything else.
|
|
259
270
|
|
|
260
271
|
|
|
@@ -334,7 +345,7 @@ queues: back*
|
|
|
334
345
|
|
|
335
346
|
Workers in Solid Queue use a thread pool to run work in multiple threads, configurable via the `threads` parameter above. Besides this, parallelism can be achieved via multiple processes on one machine (configurable via different workers or the `processes` parameter above) or by horizontal scaling.
|
|
336
347
|
|
|
337
|
-
The supervisor is in charge of managing these processes, and it responds to the following signals:
|
|
348
|
+
The supervisor is in charge of managing these processes, and it responds to the following signals when running in its own process via `bin/jobs` or with [the Puma plugin](#puma-plugin) with the default `fork` mode:
|
|
338
349
|
- `TERM`, `INT`: starts graceful termination. The supervisor will send a `TERM` signal to its supervised processes, and it'll wait up to `SolidQueue.shutdown_timeout` time until they're done. If any supervised processes are still around by then, it'll send a `QUIT` signal to them to indicate they must exit.
|
|
339
350
|
- `QUIT`: starts immediate termination. The supervisor will send a `QUIT` signal to its supervised processes, causing them to exit immediately.
|
|
340
351
|
|
|
@@ -366,7 +377,7 @@ There are several settings that control how Solid Queue works that you can set a
|
|
|
366
377
|
|
|
367
378
|
**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.
|
|
368
379
|
|
|
369
|
-
- `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 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.
|
|
380
|
+
- `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 only need to set this to `false` if your database doesn't support it. For MySQL, that'd be versions < 8; for MariaDB, versions < 10.6; and for PostgreSQL, versions < 9.5. If you use SQLite, this has no effect, as writes are sequential.
|
|
370
381
|
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
|
371
382
|
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
|
372
383
|
- `shutdown_timeout`: time the supervisor will wait since it sent the `TERM` signal to its supervised processes before sending a `QUIT` version to them requesting immediate termination—defaults to 5 seconds.
|
|
@@ -519,11 +530,11 @@ DeliverAnnouncementToContactJob.set(wait: 30.minutes).perform_later(contact)
|
|
|
519
530
|
|
|
520
531
|
The 3 jobs will go into the scheduled queue and will wait there until they're due. Then, 10 minutes after, the first two jobs will be enqueued and the second one most likely will be blocked because the first one will be running first. Then, assuming the jobs are fast and finish in a few seconds, when the third job is due, it'll be enqueued normally.
|
|
521
532
|
|
|
522
|
-
Normally scheduled jobs are enqueued in batches, but with concurrency controls, jobs need to be enqueued one by one. This has an impact on performance, similarly to the impact of concurrency controls in bulk enqueuing. Read below for more details.
|
|
533
|
+
Normally scheduled jobs are enqueued in batches, but with concurrency controls, jobs need to be enqueued one by one. This has an impact on performance, similarly to the impact of concurrency controls in bulk enqueuing. Read below for more details. We generally advise against mixing concurrency controls with waiting/scheduling in the future.
|
|
523
534
|
|
|
524
535
|
### Performance considerations
|
|
525
536
|
|
|
526
|
-
Concurrency controls introduce significant overhead (blocked executions need to be created and promoted to ready, semaphores need to be created and updated) so you should consider carefully whether you need them. For throttling purposes, where you plan to have `limit` significantly larger than 1,
|
|
537
|
+
Concurrency controls introduce significant overhead (blocked executions need to be created and promoted to ready, semaphores need to be created and updated) so you should consider carefully whether you need them. For throttling purposes, where you plan to have `limit` significantly larger than 1, we encourage relying on a limited number of workers per queue instead. For example:
|
|
527
538
|
|
|
528
539
|
```ruby
|
|
529
540
|
class ThrottledJob < ApplicationJob
|
|
@@ -603,6 +614,22 @@ that you set in production only. This is what Rails 8's default Puma config look
|
|
|
603
614
|
|
|
604
615
|
**Note**: phased restarts are not supported currently because the plugin requires [app preloading](https://github.com/puma/puma?tab=readme-ov-file#cluster-mode) to work.
|
|
605
616
|
|
|
617
|
+
### Running as a fork or asynchronously
|
|
618
|
+
|
|
619
|
+
By default, the Puma plugin will fork additional processes for each worker and dispatcher so that they run in different processes. This provides the best isolation and performance, but can have additional memory usage.
|
|
620
|
+
|
|
621
|
+
Alternatively, workers and dispatchers can be run within the same Puma process(s). To do so just configure the plugin as:
|
|
622
|
+
|
|
623
|
+
```ruby
|
|
624
|
+
plugin :solid_queue
|
|
625
|
+
solid_queue_mode :async
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Note that in this case, the `processes` configuration option will be ignored. See also [Fork vs. async mode](#fork-vs-async-mode).
|
|
629
|
+
|
|
630
|
+
**The recommended and default mode is `fork`. Only use `async` if you know what you're doing and have strong reasons to**
|
|
631
|
+
|
|
632
|
+
|
|
606
633
|
## Jobs and transactional integrity
|
|
607
634
|
:warning: Having your jobs in the same ACID-compliant database as your application data enables a powerful yet sharp tool: taking advantage of transactional integrity to ensure some action in your app is not committed unless your job is also committed and vice versa, and ensuring that your job won't be enqueued until the transaction within which you're enqueuing it is committed. This can be very powerful and useful, but it can also backfire if you base some of your logic on this behaviour, and in the future, you move to another active job backend, or if you simply move Solid Queue to its own database, and suddenly the behaviour changes under you. Because this can be quite tricky and many people shouldn't need to worry about it, by default Solid Queue is configured in a different database as the main app.
|
|
608
635
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
require "puma/plugin"
|
|
2
2
|
|
|
3
|
+
module Puma
|
|
4
|
+
class DSL
|
|
5
|
+
def solid_queue_mode(mode = :fork)
|
|
6
|
+
@options[:solid_queue_mode] = mode.to_sym
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
Puma::Plugin.create do
|
|
4
12
|
attr_reader :puma_pid, :solid_queue_pid, :log_writer, :solid_queue_supervisor
|
|
5
13
|
|
|
@@ -7,35 +15,73 @@ Puma::Plugin.create do
|
|
|
7
15
|
@log_writer = launcher.log_writer
|
|
8
16
|
@puma_pid = $$
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
if launcher.options[:solid_queue_mode] == :async
|
|
19
|
+
start_async(launcher)
|
|
20
|
+
else
|
|
21
|
+
start_forked(launcher)
|
|
12
22
|
end
|
|
23
|
+
end
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
private
|
|
26
|
+
def start_forked(launcher)
|
|
27
|
+
in_background do
|
|
28
|
+
monitor_solid_queue
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new("7")
|
|
32
|
+
launcher.events.on_booted do
|
|
33
|
+
@solid_queue_pid = fork do
|
|
34
|
+
Thread.new { monitor_puma }
|
|
35
|
+
SolidQueue::Supervisor.start(mode: :fork)
|
|
36
|
+
end
|
|
19
37
|
end
|
|
38
|
+
|
|
39
|
+
launcher.events.on_stopped { stop_solid_queue_fork }
|
|
40
|
+
launcher.events.on_restart { stop_solid_queue_fork }
|
|
41
|
+
else
|
|
42
|
+
launcher.events.after_booted do
|
|
43
|
+
@solid_queue_pid = fork do
|
|
44
|
+
Thread.new { monitor_puma }
|
|
45
|
+
start_solid_queue(mode: :fork)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
launcher.events.after_stopped { stop_solid_queue_fork }
|
|
50
|
+
launcher.events.before_restart { stop_solid_queue_fork }
|
|
20
51
|
end
|
|
52
|
+
end
|
|
21
53
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
54
|
+
def start_async(launcher)
|
|
55
|
+
if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new("7")
|
|
56
|
+
launcher.events.on_booted do
|
|
57
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
launcher.events.on_stopped { solid_queue_supervisor&.stop }
|
|
61
|
+
|
|
62
|
+
launcher.events.on_restart do
|
|
63
|
+
solid_queue_supervisor&.stop
|
|
64
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
launcher.events.after_booted do
|
|
68
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
launcher.events.after_stopped { solid_queue_supervisor&.stop }
|
|
72
|
+
|
|
73
|
+
launcher.events.before_restart do
|
|
74
|
+
solid_queue_supervisor&.stop
|
|
75
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
29
76
|
end
|
|
30
77
|
end
|
|
78
|
+
end
|
|
31
79
|
|
|
32
|
-
|
|
33
|
-
|
|
80
|
+
def start_solid_queue(**options)
|
|
81
|
+
@solid_queue_supervisor = SolidQueue::Supervisor.start(**options)
|
|
34
82
|
end
|
|
35
|
-
end
|
|
36
83
|
|
|
37
|
-
|
|
38
|
-
def stop_solid_queue
|
|
84
|
+
def stop_solid_queue_fork
|
|
39
85
|
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
|
40
86
|
log "Stopping Solid Queue..."
|
|
41
87
|
Process.kill(:INT, solid_queue_pid) if solid_queue_pid
|
|
@@ -48,7 +94,7 @@ Puma::Plugin.create do
|
|
|
48
94
|
end
|
|
49
95
|
|
|
50
96
|
def monitor_solid_queue
|
|
51
|
-
monitor(:
|
|
97
|
+
monitor(:solid_queue_fork_dead?, "Detected Solid Queue has gone away, stopping Puma...")
|
|
52
98
|
end
|
|
53
99
|
|
|
54
100
|
def monitor(process_dead, message)
|
|
@@ -62,7 +108,7 @@ Puma::Plugin.create do
|
|
|
62
108
|
end
|
|
63
109
|
end
|
|
64
110
|
|
|
65
|
-
def
|
|
111
|
+
def solid_queue_fork_dead?
|
|
66
112
|
if solid_queue_started?
|
|
67
113
|
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
|
68
114
|
end
|
|
@@ -17,5 +17,15 @@ module SolidQueue
|
|
|
17
17
|
SolidQueue.on_thread_error.call(error)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
def create_thread(&block)
|
|
22
|
+
Thread.new do
|
|
23
|
+
Thread.current.name = name
|
|
24
|
+
block.call
|
|
25
|
+
rescue Exception => exception
|
|
26
|
+
handle_thread_error(exception)
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
end
|
|
20
30
|
end
|
|
21
31
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class AsyncSupervisor < Supervisor
|
|
5
|
+
after_shutdown :terminate_gracefully, unless: :standalone?
|
|
6
|
+
|
|
7
|
+
def stop
|
|
8
|
+
super
|
|
9
|
+
@thread&.join
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
def supervise
|
|
14
|
+
if standalone? then super
|
|
15
|
+
else
|
|
16
|
+
@thread = create_thread { super }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check_and_replace_terminated_processes
|
|
21
|
+
terminated_threads = process_instances.select { |thread_id, instance| !instance.alive? }
|
|
22
|
+
terminated_threads.each { |thread_id, instance| replace_thread(thread_id, instance) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def replace_thread(thread_id, instance)
|
|
26
|
+
SolidQueue.instrument(:replace_thread, supervisor_pid: ::Process.pid) do |payload|
|
|
27
|
+
payload[:thread] = instance
|
|
28
|
+
|
|
29
|
+
error = Processes::ThreadTerminatedError.new(terminated_instance.name)
|
|
30
|
+
release_claimed_jobs_by(terminated_instance, with_error: error)
|
|
31
|
+
|
|
32
|
+
start_process(configured_processes.delete(thread_id))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def perform_graceful_termination
|
|
37
|
+
process_instances.values.each(&:stop)
|
|
38
|
+
|
|
39
|
+
Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_processes_terminated? })
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def perform_immediate_termination
|
|
43
|
+
exit!
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def all_processes_terminated?
|
|
47
|
+
process_instances.values.none?(&:alive?)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/solid_queue/cli.rb
CHANGED
|
@@ -8,6 +8,10 @@ module SolidQueue
|
|
|
8
8
|
desc: "Path to config file (default: #{Configuration::DEFAULT_CONFIG_FILE_PATH}).",
|
|
9
9
|
banner: "SOLID_QUEUE_CONFIG"
|
|
10
10
|
|
|
11
|
+
class_option :mode, type: :string, default: "fork", enum: %w[ fork async ],
|
|
12
|
+
desc: "Whether to fork processes for workers and dispatchers (fork) or to run these in the same process as the supervisor (async) (default: fork).",
|
|
13
|
+
banner: "SOLID_QUEUE_SUPERVISOR_MODE"
|
|
14
|
+
|
|
11
15
|
class_option :recurring_schedule_file, type: :string,
|
|
12
16
|
desc: "Path to recurring schedule definition (default: #{Configuration::DEFAULT_RECURRING_SCHEDULE_FILE_PATH}).",
|
|
13
17
|
banner: "SOLID_QUEUE_RECURRING_SCHEDULE"
|
|
@@ -56,6 +56,14 @@ module SolidQueue
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
def mode
|
|
60
|
+
@options[:mode].to_s.inquiry
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def standalone?
|
|
64
|
+
mode.fork? || @options[:standalone]
|
|
65
|
+
end
|
|
66
|
+
|
|
59
67
|
private
|
|
60
68
|
attr_reader :options
|
|
61
69
|
|
|
@@ -84,6 +92,8 @@ module SolidQueue
|
|
|
84
92
|
|
|
85
93
|
def default_options
|
|
86
94
|
{
|
|
95
|
+
mode: ENV["SOLID_QUEUE_SUPERVISOR_MODE"] || :fork,
|
|
96
|
+
standalone: true,
|
|
87
97
|
config_file: Rails.root.join(ENV["SOLID_QUEUE_CONFIG"] || DEFAULT_CONFIG_FILE_PATH),
|
|
88
98
|
recurring_schedule_file: Rails.root.join(ENV["SOLID_QUEUE_RECURRING_SCHEDULE"] || DEFAULT_RECURRING_SCHEDULE_FILE_PATH),
|
|
89
99
|
only_work: false,
|
|
@@ -110,7 +120,12 @@ module SolidQueue
|
|
|
110
120
|
|
|
111
121
|
def workers
|
|
112
122
|
workers_options.flat_map do |worker_options|
|
|
113
|
-
processes =
|
|
123
|
+
processes = if mode.fork?
|
|
124
|
+
worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
|
|
125
|
+
else
|
|
126
|
+
1
|
|
127
|
+
end
|
|
128
|
+
|
|
114
129
|
processes.times.map { Process.new(:worker, worker_options.with_defaults(WORKER_DEFAULTS)) }
|
|
115
130
|
end
|
|
116
131
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class ForkSupervisor < Supervisor
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def perform_graceful_termination
|
|
8
|
+
term_forks
|
|
9
|
+
|
|
10
|
+
Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_processes_terminated? }) do
|
|
11
|
+
reap_terminated_forks
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def perform_immediate_termination
|
|
16
|
+
quit_forks
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def term_forks
|
|
20
|
+
signal_processes(process_instances.keys, :TERM)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def quit_forks
|
|
24
|
+
signal_processes(process_instances.keys, :QUIT)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def check_and_replace_terminated_processes
|
|
28
|
+
loop do
|
|
29
|
+
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
30
|
+
break unless pid
|
|
31
|
+
|
|
32
|
+
replace_fork(pid, status)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reap_terminated_forks
|
|
37
|
+
loop do
|
|
38
|
+
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
39
|
+
break unless pid
|
|
40
|
+
|
|
41
|
+
if (terminated_fork = process_instances.delete(pid)) && !status.exited? || status.exitstatus > 0
|
|
42
|
+
error = Processes::ProcessExitError.new(status)
|
|
43
|
+
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
configured_processes.delete(pid)
|
|
47
|
+
end
|
|
48
|
+
rescue SystemCallError
|
|
49
|
+
# All children already reaped
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def replace_fork(pid, status)
|
|
53
|
+
SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
|
|
54
|
+
if terminated_fork = process_instances.delete(pid)
|
|
55
|
+
payload[:fork] = terminated_fork
|
|
56
|
+
error = Processes::ProcessExitError.new(status)
|
|
57
|
+
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
58
|
+
|
|
59
|
+
start_process(configured_processes.delete(pid))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def all_processes_terminated?
|
|
65
|
+
process_instances.empty?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -7,20 +7,26 @@ module SolidQueue::Processes
|
|
|
7
7
|
attr_writer :mode
|
|
8
8
|
|
|
9
9
|
def start
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if running_async?
|
|
13
|
-
@thread = create_thread { run }
|
|
14
|
-
else
|
|
10
|
+
run_in_mode do
|
|
11
|
+
boot
|
|
15
12
|
run
|
|
16
13
|
end
|
|
17
14
|
end
|
|
18
15
|
|
|
19
16
|
def stop
|
|
20
17
|
super
|
|
21
|
-
|
|
22
18
|
wake_up
|
|
23
|
-
|
|
19
|
+
|
|
20
|
+
# When not supervised, block until the thread terminates for backward
|
|
21
|
+
# compatibility with code that expects stop to be synchronous.
|
|
22
|
+
# When supervised, the supervisor controls the shutdown timeout.
|
|
23
|
+
unless supervised?
|
|
24
|
+
@thread&.join
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def alive?
|
|
29
|
+
!running_async? || @thread&.alive?
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
private
|
|
@@ -30,6 +36,18 @@ module SolidQueue::Processes
|
|
|
30
36
|
(@mode || DEFAULT_MODE).to_s.inquiry
|
|
31
37
|
end
|
|
32
38
|
|
|
39
|
+
def run_in_mode(&block)
|
|
40
|
+
case
|
|
41
|
+
when running_as_fork?
|
|
42
|
+
fork(&block)
|
|
43
|
+
when running_async?
|
|
44
|
+
@thread = create_thread(&block)
|
|
45
|
+
@thread.object_id
|
|
46
|
+
else
|
|
47
|
+
block.call
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
33
51
|
def boot
|
|
34
52
|
SolidQueue.instrument(:start_process, process: self) do
|
|
35
53
|
run_callbacks(:boot) do
|
|
@@ -74,16 +92,5 @@ module SolidQueue::Processes
|
|
|
74
92
|
def running_as_fork?
|
|
75
93
|
mode.fork?
|
|
76
94
|
end
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def create_thread(&block)
|
|
80
|
-
Thread.new do
|
|
81
|
-
Thread.current.name = name
|
|
82
|
-
block.call
|
|
83
|
-
rescue Exception => exception
|
|
84
|
-
handle_thread_error(exception)
|
|
85
|
-
raise
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
95
|
end
|
|
89
96
|
end
|
|
@@ -32,5 +32,16 @@ module SolidQueue
|
|
|
32
32
|
ClaimedExecution.orphaned.fail_all_with(Processes::ProcessMissingError.new)
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
# When a supervised process crashes or exits we need to mark all the
|
|
37
|
+
# executions it had claimed as failed so that they can be retried
|
|
38
|
+
# by some other worker.
|
|
39
|
+
def release_claimed_jobs_by(terminated_process, with_error:)
|
|
40
|
+
wrap_in_app_executor do
|
|
41
|
+
if registered_process = SolidQueue::Process.find_by(name: terminated_process.name)
|
|
42
|
+
registered_process.fail_all_claimed_executions_with(with_error)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
35
46
|
end
|
|
36
47
|
end
|
|
@@ -6,8 +6,8 @@ module SolidQueue
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
-
before_boot :register_signal_handlers
|
|
10
|
-
after_shutdown :restore_default_signal_handlers
|
|
9
|
+
before_boot :register_signal_handlers, if: :standalone?
|
|
10
|
+
after_shutdown :restore_default_signal_handlers, if: :standalone?
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
private
|
|
@@ -13,17 +13,21 @@ module SolidQueue
|
|
|
13
13
|
configuration = Configuration.new(**options)
|
|
14
14
|
|
|
15
15
|
if configuration.valid?
|
|
16
|
-
|
|
16
|
+
klass = configuration.mode.fork? ? ForkSupervisor : AsyncSupervisor
|
|
17
|
+
klass.new(configuration).tap(&:start)
|
|
17
18
|
else
|
|
18
19
|
abort configuration.errors.full_messages.join("\n") + "\nExiting..."
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
|
|
24
|
+
delegate :mode, :standalone?, to: :configuration
|
|
25
|
+
|
|
23
26
|
def initialize(configuration)
|
|
24
27
|
@configuration = configuration
|
|
25
|
-
|
|
28
|
+
|
|
26
29
|
@configured_processes = {}
|
|
30
|
+
@process_instances = {}
|
|
27
31
|
|
|
28
32
|
super
|
|
29
33
|
end
|
|
@@ -43,8 +47,12 @@ module SolidQueue
|
|
|
43
47
|
run_stop_hooks
|
|
44
48
|
end
|
|
45
49
|
|
|
50
|
+
def kind
|
|
51
|
+
"Supervisor(#{mode})"
|
|
52
|
+
end
|
|
53
|
+
|
|
46
54
|
private
|
|
47
|
-
attr_reader :configuration, :
|
|
55
|
+
attr_reader :configuration, :configured_processes, :process_instances
|
|
48
56
|
|
|
49
57
|
def boot
|
|
50
58
|
SolidQueue.instrument(:start_process, process: self) do
|
|
@@ -62,11 +70,13 @@ module SolidQueue
|
|
|
62
70
|
loop do
|
|
63
71
|
break if stopped?
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
if standalone?
|
|
74
|
+
set_procline
|
|
75
|
+
process_signal_queue
|
|
76
|
+
end
|
|
67
77
|
|
|
68
78
|
unless stopped?
|
|
69
|
-
|
|
79
|
+
check_and_replace_terminated_processes
|
|
70
80
|
interruptible_sleep(1.second)
|
|
71
81
|
end
|
|
72
82
|
end
|
|
@@ -77,30 +87,23 @@ module SolidQueue
|
|
|
77
87
|
def start_process(configured_process)
|
|
78
88
|
process_instance = configured_process.instantiate.tap do |instance|
|
|
79
89
|
instance.supervised_by process
|
|
80
|
-
instance.mode =
|
|
90
|
+
instance.mode = mode
|
|
81
91
|
end
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
process_instance.start
|
|
85
|
-
end
|
|
93
|
+
process_id = process_instance.start
|
|
86
94
|
|
|
87
|
-
configured_processes[
|
|
88
|
-
|
|
95
|
+
configured_processes[process_id] = configured_process
|
|
96
|
+
process_instances[process_id] = process_instance
|
|
89
97
|
end
|
|
90
98
|
|
|
91
|
-
def
|
|
92
|
-
procline "supervising #{supervised_processes.join(", ")}"
|
|
99
|
+
def check_and_replace_terminated_processes
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
def terminate_gracefully
|
|
96
|
-
SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
|
|
100
|
-
reap_terminated_forks
|
|
101
|
-
end
|
|
103
|
+
SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: configured_processes.keys) do |payload|
|
|
104
|
+
perform_graceful_termination
|
|
102
105
|
|
|
103
|
-
unless
|
|
106
|
+
unless all_processes_terminated?
|
|
104
107
|
payload[:shutdown_timeout_exceeded] = true
|
|
105
108
|
terminate_immediately
|
|
106
109
|
end
|
|
@@ -108,84 +111,37 @@ module SolidQueue
|
|
|
108
111
|
end
|
|
109
112
|
|
|
110
113
|
def terminate_immediately
|
|
111
|
-
SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes:
|
|
112
|
-
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def shutdown
|
|
117
|
-
SolidQueue.instrument(:shutdown_process, process: self) do
|
|
118
|
-
run_callbacks(:shutdown) do
|
|
119
|
-
stop_maintenance_task
|
|
120
|
-
end
|
|
114
|
+
SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: configured_processes.keys) do
|
|
115
|
+
perform_immediate_termination
|
|
121
116
|
end
|
|
122
117
|
end
|
|
123
118
|
|
|
124
|
-
def
|
|
125
|
-
|
|
119
|
+
def perform_graceful_termination
|
|
120
|
+
raise NotImplementedError
|
|
126
121
|
end
|
|
127
122
|
|
|
128
|
-
def
|
|
129
|
-
|
|
123
|
+
def perform_immediate_termination
|
|
124
|
+
raise NotImplementedError
|
|
130
125
|
end
|
|
131
126
|
|
|
132
|
-
def
|
|
133
|
-
|
|
127
|
+
def all_processes_terminated?
|
|
128
|
+
raise NotImplementedError
|
|
134
129
|
end
|
|
135
130
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def reap_and_replace_terminated_forks
|
|
141
|
-
loop do
|
|
142
|
-
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
143
|
-
break unless pid
|
|
144
|
-
|
|
145
|
-
replace_fork(pid, status)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def reap_terminated_forks
|
|
150
|
-
loop do
|
|
151
|
-
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
152
|
-
break unless pid
|
|
153
|
-
|
|
154
|
-
if (terminated_fork = forks.delete(pid)) && (!status.exited? || status.exitstatus > 0)
|
|
155
|
-
handle_claimed_jobs_by(terminated_fork, status)
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
configured_processes.delete(pid)
|
|
159
|
-
end
|
|
160
|
-
rescue SystemCallError
|
|
161
|
-
# All children already reaped
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def replace_fork(pid, status)
|
|
165
|
-
SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
|
|
166
|
-
if terminated_fork = forks.delete(pid)
|
|
167
|
-
payload[:fork] = terminated_fork
|
|
168
|
-
handle_claimed_jobs_by(terminated_fork, status)
|
|
169
|
-
|
|
170
|
-
start_process(configured_processes.delete(pid))
|
|
131
|
+
def shutdown
|
|
132
|
+
SolidQueue.instrument(:shutdown_process, process: self) do
|
|
133
|
+
run_callbacks(:shutdown) do
|
|
134
|
+
stop_maintenance_task
|
|
171
135
|
end
|
|
172
136
|
end
|
|
173
137
|
end
|
|
174
138
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# by some other worker.
|
|
178
|
-
def handle_claimed_jobs_by(terminated_fork, status)
|
|
179
|
-
wrap_in_app_executor do
|
|
180
|
-
if registered_process = SolidQueue::Process.find_by(name: terminated_fork.name)
|
|
181
|
-
error = Processes::ProcessExitError.new(status)
|
|
182
|
-
registered_process.fail_all_claimed_executions_with(error)
|
|
183
|
-
end
|
|
184
|
-
end
|
|
139
|
+
def set_procline
|
|
140
|
+
procline "supervising #{configured_processes.keys.join(", ")}"
|
|
185
141
|
end
|
|
186
142
|
|
|
187
|
-
def
|
|
188
|
-
|
|
143
|
+
def sync_std_streams
|
|
144
|
+
STDOUT.sync = STDERR.sync = true
|
|
189
145
|
end
|
|
190
146
|
end
|
|
191
147
|
end
|
data/lib/solid_queue/timer.rb
CHANGED
|
@@ -4,18 +4,18 @@ module SolidQueue
|
|
|
4
4
|
module Timer
|
|
5
5
|
extend self
|
|
6
6
|
|
|
7
|
-
def wait_until(timeout, condition
|
|
7
|
+
def wait_until(timeout, condition)
|
|
8
8
|
if timeout > 0
|
|
9
9
|
deadline = monotonic_time_now + timeout
|
|
10
10
|
|
|
11
11
|
while monotonic_time_now < deadline && !condition.call
|
|
12
12
|
sleep 0.1
|
|
13
|
-
|
|
13
|
+
yield if block_given?
|
|
14
14
|
end
|
|
15
15
|
else
|
|
16
16
|
while !condition.call
|
|
17
17
|
sleep 0.5
|
|
18
|
-
|
|
18
|
+
yield if block_given?
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
data/lib/solid_queue/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_queue
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rosa Gutierrez
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -122,6 +122,20 @@ dependencies:
|
|
|
122
122
|
- - "~>"
|
|
123
123
|
- !ruby/object:Gem::Version
|
|
124
124
|
version: '1.9'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: minitest
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '5.0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '5.0'
|
|
125
139
|
- !ruby/object:Gem::Dependency
|
|
126
140
|
name: mocha
|
|
127
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -297,11 +311,13 @@ files:
|
|
|
297
311
|
- lib/puma/plugin/solid_queue.rb
|
|
298
312
|
- lib/solid_queue.rb
|
|
299
313
|
- lib/solid_queue/app_executor.rb
|
|
314
|
+
- lib/solid_queue/async_supervisor.rb
|
|
300
315
|
- lib/solid_queue/cli.rb
|
|
301
316
|
- lib/solid_queue/configuration.rb
|
|
302
317
|
- lib/solid_queue/dispatcher.rb
|
|
303
318
|
- lib/solid_queue/dispatcher/concurrency_maintenance.rb
|
|
304
319
|
- lib/solid_queue/engine.rb
|
|
320
|
+
- lib/solid_queue/fork_supervisor.rb
|
|
305
321
|
- lib/solid_queue/lifecycle_hooks.rb
|
|
306
322
|
- lib/solid_queue/log_subscriber.rb
|
|
307
323
|
- lib/solid_queue/pool.rb
|
|
@@ -316,6 +332,7 @@ files:
|
|
|
316
332
|
- lib/solid_queue/processes/registrable.rb
|
|
317
333
|
- lib/solid_queue/processes/runnable.rb
|
|
318
334
|
- lib/solid_queue/processes/supervised.rb
|
|
335
|
+
- lib/solid_queue/processes/thread_terminated_error.rb
|
|
319
336
|
- lib/solid_queue/scheduler.rb
|
|
320
337
|
- lib/solid_queue/scheduler/recurring_schedule.rb
|
|
321
338
|
- lib/solid_queue/supervisor.rb
|