solid_queue 1.2.4 → 1.4.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 +99 -19
- data/app/models/solid_queue/blocked_execution.rb +3 -1
- data/app/models/solid_queue/failed_execution.rb +1 -1
- data/app/models/solid_queue/job/concurrency_controls.rb +1 -1
- data/app/models/solid_queue/job/retryable.rb +10 -1
- data/app/models/solid_queue/ready_execution.rb +2 -1
- data/app/models/solid_queue/record.rb +7 -0
- data/app/models/solid_queue/recurring_task.rb +25 -7
- data/app/models/solid_queue/semaphore.rb +1 -1
- data/lib/puma/plugin/solid_queue.rb +70 -22
- data/lib/solid_queue/app_executor.rb +10 -0
- data/lib/solid_queue/async_supervisor.rb +52 -0
- data/lib/solid_queue/cli.rb +4 -0
- data/lib/solid_queue/configuration.rb +41 -8
- data/lib/solid_queue/dispatcher.rb +1 -0
- data/lib/solid_queue/fork_supervisor.rb +68 -0
- data/lib/solid_queue/processes/registrable.rb +5 -1
- data/lib/solid_queue/processes/runnable.rb +25 -18
- data/lib/solid_queue/processes/thread_terminated_error.rb +11 -0
- data/lib/solid_queue/scheduler/recurring_schedule.rb +61 -11
- data/lib/solid_queue/scheduler.rb +23 -4
- 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
- data/lib/solid_queue.rb +8 -0
- 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: fd0590f46160c60f3a496158cf8dc2412803025cd06d94ac423c32f0b688dd77
|
|
4
|
+
data.tar.gz: 8c892f457280b1974908d2de0c3e5be5229ff6bcb448ed61c2937ff4566da796
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a86b46d35b0cc8aef4a235e401b4470a6f6e64ff8c44ebdefc06f597d6cbe67ea76c786fc1e85caee1c3fc4dd492180530348078890ef74af90461705150a60
|
|
7
|
+
data.tar.gz: efeadbeb7dc0d044c801915d6ba4d8c249b249ef0dde8726454a359d3f1a4ce9ad4e2edb6cd19f8c371967059081c0ef2904ea630a0ae620123939ce881f0fdb
|
data/README.md
CHANGED
|
@@ -14,8 +14,10 @@ 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
|
-
- [
|
|
20
|
+
- [Optional scheduler configuration](#optional-scheduler-configuration)
|
|
19
21
|
- [Queue order and priorities](#queue-order-and-priorities)
|
|
20
22
|
- [Queues specification and performance](#queues-specification-and-performance)
|
|
21
23
|
- [Threads, processes, and signals](#threads-processes-and-signals)
|
|
@@ -30,6 +32,7 @@ Solid Queue can be used with SQL databases such as MySQL, PostgreSQL, or SQLite,
|
|
|
30
32
|
- [Puma plugin](#puma-plugin)
|
|
31
33
|
- [Jobs and transactional integrity](#jobs-and-transactional-integrity)
|
|
32
34
|
- [Recurring tasks](#recurring-tasks)
|
|
35
|
+
- [Scheduling and unscheduling recurring tasks dynamically](#scheduling-and-unscheduling-recurring-tasks-dynamically)
|
|
33
36
|
- [Inspiration](#inspiration)
|
|
34
37
|
- [License](#license)
|
|
35
38
|
|
|
@@ -92,10 +95,10 @@ development:
|
|
|
92
95
|
+ primary:
|
|
93
96
|
<<: *default
|
|
94
97
|
database: storage/development.sqlite3
|
|
95
|
-
+
|
|
96
|
-
+
|
|
97
|
-
+
|
|
98
|
-
+
|
|
98
|
+
+ queue:
|
|
99
|
+
+ <<: *default
|
|
100
|
+
+ database: storage/development_queue.sqlite3
|
|
101
|
+
+ migrations_paths: db/queue_migrate
|
|
99
102
|
```
|
|
100
103
|
|
|
101
104
|
Next, add the following to `development.rb`
|
|
@@ -127,7 +130,7 @@ In `config/cable.yml`
|
|
|
127
130
|
```diff
|
|
128
131
|
development:
|
|
129
132
|
- adapter: async
|
|
130
|
-
+
|
|
133
|
+
+ adapter: solid_cable
|
|
131
134
|
+ connects_to:
|
|
132
135
|
+ database:
|
|
133
136
|
+ writing: cable
|
|
@@ -177,11 +180,9 @@ end
|
|
|
177
180
|
|
|
178
181
|
### High performance requirements
|
|
179
182
|
|
|
180
|
-
Solid Queue was designed for the highest throughput when used with MySQL 8
|
|
181
|
-
|
|
182
|
-
## Configuration
|
|
183
|
+
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
184
|
|
|
184
|
-
|
|
185
|
+
## Workers, dispatchers, and scheduler
|
|
185
186
|
|
|
186
187
|
We have several types of actors in Solid Queue:
|
|
187
188
|
|
|
@@ -190,7 +191,19 @@ We have several types of actors in Solid Queue:
|
|
|
190
191
|
- The _scheduler_ manages [recurring tasks](#recurring-tasks), enqueuing jobs for them when they're due.
|
|
191
192
|
- The _supervisor_ runs workers and dispatchers according to the configuration, controls their heartbeats, and stops and starts them when needed.
|
|
192
193
|
|
|
193
|
-
|
|
194
|
+
### Fork vs. async mode
|
|
195
|
+
|
|
196
|
+
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:
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
bin/jobs --mode async
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
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.
|
|
203
|
+
|
|
204
|
+
**The recommended and default mode is `fork`. Only use `async` if you know what you're doing and have strong reasons to**
|
|
205
|
+
|
|
206
|
+
## Configuration
|
|
194
207
|
|
|
195
208
|
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
209
|
|
|
@@ -198,7 +211,7 @@ By default, Solid Queue will try to find your configuration under `config/queue.
|
|
|
198
211
|
bin/jobs -c config/calendar.yml
|
|
199
212
|
```
|
|
200
213
|
|
|
201
|
-
You can also skip
|
|
214
|
+
You can also skip the scheduler process by setting the environment variable `SOLID_QUEUE_SKIP_RECURRING=true`. This is useful for environments like staging, review apps, or development where you don't want any recurring jobs to run. This is equivalent to using the `--skip-recurring` option with `bin/jobs`.
|
|
202
215
|
|
|
203
216
|
This is what this configuration looks like:
|
|
204
217
|
|
|
@@ -216,6 +229,10 @@ production:
|
|
|
216
229
|
threads: 5
|
|
217
230
|
polling_interval: 0.1
|
|
218
231
|
processes: 3
|
|
232
|
+
scheduler:
|
|
233
|
+
dynamic_tasks_enabled: true
|
|
234
|
+
polling_interval: 5
|
|
235
|
+
|
|
219
236
|
```
|
|
220
237
|
|
|
221
238
|
Everything is optional. If no configuration at all is provided, Solid Queue will run with one dispatcher and one worker with default settings. If you want to run only dispatchers or workers, you just need to include that section alone in the configuration. For example, with the following configuration:
|
|
@@ -247,6 +264,8 @@ Here's an overview of the different options:
|
|
|
247
264
|
```
|
|
248
265
|
|
|
249
266
|
This will create a worker fetching jobs from all queues starting with `staging`. The wildcard `*` is only allowed on its own or at the end of a queue name; you can't specify queue names such as `*_some_queue`. These will be ignored.
|
|
267
|
+
|
|
268
|
+
Also, if a wildcard (*) is included alongside explicit queue names, for example: `queues: [default, backend, *]`, then it would behave like `queues: *`
|
|
250
269
|
|
|
251
270
|
Finally, you can combine prefixes with exact names, like `[ staging*, background ]`, and the behaviour with respect to order will be the same as with only exact names.
|
|
252
271
|
|
|
@@ -254,10 +273,23 @@ Here's an overview of the different options:
|
|
|
254
273
|
|
|
255
274
|
- `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
275
|
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.
|
|
276
|
+
- `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
277
|
- `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
278
|
|
|
260
279
|
|
|
280
|
+
### Optional scheduler configuration
|
|
281
|
+
|
|
282
|
+
Optionally, you can configure the scheduler process under the `scheduler` section in your `config/queue.yml` if you'd like to [schedule recurring tasks dynamically](#scheduling-and-unscheduling-recurring-tasks-dynamically).
|
|
283
|
+
|
|
284
|
+
```yaml
|
|
285
|
+
scheduler:
|
|
286
|
+
dynamic_tasks_enabled: true
|
|
287
|
+
polling_interval: 5
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- `dynamic_tasks_enabled`: whether the scheduler should poll for [dynamically scheduled recurring tasks](#scheduling-and-unscheduling-recurring-tasks-dynamically). This is `false` by default. When enabled, the scheduler will poll the database at the given `polling_interval` to pick up tasks scheduled via `SolidQueue.schedule_recurring_task`.
|
|
291
|
+
- `polling_interval`: how frequently (in seconds) the scheduler checks for dynamic task changes. Defaults to `5`.
|
|
292
|
+
|
|
261
293
|
### Queue order and priorities
|
|
262
294
|
|
|
263
295
|
As mentioned above, if you specify a list of queues for a worker, these will be polled in the order given, such as for the list `real_time,background`, no jobs will be taken from `background` unless there aren't any more jobs waiting in `real_time`.
|
|
@@ -334,7 +366,7 @@ queues: back*
|
|
|
334
366
|
|
|
335
367
|
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
368
|
|
|
337
|
-
The supervisor is in charge of managing these processes, and it responds to the following signals:
|
|
369
|
+
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
370
|
- `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
371
|
- `QUIT`: starts immediate termination. The supervisor will send a `QUIT` signal to its supervised processes, causing them to exit immediately.
|
|
340
372
|
|
|
@@ -366,7 +398,7 @@ There are several settings that control how Solid Queue works that you can set a
|
|
|
366
398
|
|
|
367
399
|
**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
400
|
|
|
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.
|
|
401
|
+
- `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
402
|
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
|
371
403
|
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
|
372
404
|
- `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.
|
|
@@ -449,7 +481,7 @@ class MyJob < ApplicationJob
|
|
|
449
481
|
- `group` is used to control the concurrency of different job classes together. It defaults to the job class name.
|
|
450
482
|
- `on_conflict` controls behaviour when enqueuing a job that conflicts with the concurrency limits configured. It can be set to one of the following:
|
|
451
483
|
- (default) `:block`: the job is blocked and is dispatched when another job completes and unblocks it, or when the duration expires.
|
|
452
|
-
- `:discard`: the job is discarded. When you choose this option, bear in mind that if a job runs and fails to remove the concurrency lock (or _semaphore_, read below to know more about this), all jobs conflicting with it will be discarded
|
|
484
|
+
- `:discard`: the job is discarded. When you choose this option, bear in mind that if a job runs and fails to remove the concurrency lock (or _semaphore_, read below to know more about this), all jobs conflicting with it will be discarded until the interval defined by `duration` has elapsed.
|
|
453
485
|
|
|
454
486
|
When a job includes these controls, we'll ensure that, at most, the number of jobs (indicated as `to`) that yield the same `key` will be performed concurrently, and this guarantee will last for `duration` for each job enqueued. Note that there's no guarantee about _the order of execution_, only about jobs being performed at the same time (overlapping).
|
|
455
487
|
|
|
@@ -459,7 +491,7 @@ Since something can happen that prevents the first job from releasing the semaph
|
|
|
459
491
|
|
|
460
492
|
It's important to note that after one or more candidate jobs are unblocked (either because a job finishes or because `duration` expires and a semaphore is released), the `duration` timer for the still blocked jobs is reset. This happens indirectly via the expiration time of the semaphore, which is updated.
|
|
461
493
|
|
|
462
|
-
When using `discard` as the behaviour to handle conflicts, you might have jobs discarded for
|
|
494
|
+
When using `discard` as the behaviour to handle conflicts, you might have jobs discarded for until the `duration` interval if something happens and a running job fails to release the semaphore.
|
|
463
495
|
|
|
464
496
|
|
|
465
497
|
For example:
|
|
@@ -519,11 +551,11 @@ DeliverAnnouncementToContactJob.set(wait: 30.minutes).perform_later(contact)
|
|
|
519
551
|
|
|
520
552
|
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
553
|
|
|
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.
|
|
554
|
+
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
555
|
|
|
524
556
|
### Performance considerations
|
|
525
557
|
|
|
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,
|
|
558
|
+
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
559
|
|
|
528
560
|
```ruby
|
|
529
561
|
class ThrottledJob < ApplicationJob
|
|
@@ -603,6 +635,22 @@ that you set in production only. This is what Rails 8's default Puma config look
|
|
|
603
635
|
|
|
604
636
|
**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
637
|
|
|
638
|
+
### Running as a fork or asynchronously
|
|
639
|
+
|
|
640
|
+
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.
|
|
641
|
+
|
|
642
|
+
Alternatively, workers and dispatchers can be run within the same Puma process(s). To do so just configure the plugin as:
|
|
643
|
+
|
|
644
|
+
```ruby
|
|
645
|
+
plugin :solid_queue
|
|
646
|
+
solid_queue_mode :async
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
Note that in this case, the `processes` configuration option will be ignored. See also [Fork vs. async mode](#fork-vs-async-mode).
|
|
650
|
+
|
|
651
|
+
**The recommended and default mode is `fork`. Only use `async` if you know what you're doing and have strong reasons to**
|
|
652
|
+
|
|
653
|
+
|
|
606
654
|
## Jobs and transactional integrity
|
|
607
655
|
: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
656
|
|
|
@@ -703,6 +751,38 @@ my_periodic_resque_job:
|
|
|
703
751
|
|
|
704
752
|
and the job will be enqueued via `perform_later` so it'll run in Resque. However, in this case we won't track any `solid_queue_recurring_execution` record for it and there won't be any guarantees that the job is enqueued only once each time.
|
|
705
753
|
|
|
754
|
+
### Scheduling and unscheduling recurring tasks dynamically
|
|
755
|
+
|
|
756
|
+
You can schedule and unschedule recurring tasks at runtime, without editing the configuration file. To enable this, you need to set `dynamic_tasks_enabled: true` in the `scheduler` section of your `config/queue.yml`, [as explained earlier](#optional-scheduler-configuration).
|
|
757
|
+
|
|
758
|
+
```yaml
|
|
759
|
+
scheduler:
|
|
760
|
+
dynamic_tasks_enabled: true
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
Then you can use the following methods to add recurring tasks dynamically:
|
|
764
|
+
|
|
765
|
+
```ruby
|
|
766
|
+
SolidQueue.schedule_recurring_task(
|
|
767
|
+
"my_dynamic_task",
|
|
768
|
+
class: "MyJob",
|
|
769
|
+
args: [1, 2],
|
|
770
|
+
schedule: "every 10 minutes"
|
|
771
|
+
)
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
This accepts the same options as the YAML configuration: `class`, `args`, `command`, `schedule`, `queue`, `priority`, and `description`.
|
|
775
|
+
|
|
776
|
+
To remove a dynamically scheduled task:
|
|
777
|
+
|
|
778
|
+
```ruby
|
|
779
|
+
SolidQueue.unschedule_recurring_task("my_dynamic_task")
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Only dynamic tasks can be unscheduled at runtime. Attempting to unschedule a static task (defined in `config/recurring.yml`) will raise an `ActiveRecord::RecordNotFound` error.
|
|
783
|
+
|
|
784
|
+
Tasks scheduled like this persist between Solid Queue's restarts and won't stop running until you manually unschedule them.
|
|
785
|
+
|
|
706
786
|
## Inspiration
|
|
707
787
|
|
|
708
788
|
Solid Queue has been inspired by [resque](https://github.com/resque/resque) and [GoodJob](https://github.com/bensheldon/good_job). We recommend checking out these projects as they're great examples from which we've learnt a lot.
|
|
@@ -26,7 +26,9 @@ module SolidQueue
|
|
|
26
26
|
|
|
27
27
|
def release_one(concurrency_key)
|
|
28
28
|
transaction do
|
|
29
|
-
if execution = ordered.where(concurrency_key: concurrency_key).limit(1)
|
|
29
|
+
if execution = ordered.where(concurrency_key: concurrency_key).limit(1)
|
|
30
|
+
.use_index(:index_solid_queue_blocked_executions_for_release)
|
|
31
|
+
.non_blocking_lock.first
|
|
30
32
|
execution.release
|
|
31
33
|
end
|
|
32
34
|
end
|
|
@@ -16,7 +16,16 @@ module SolidQueue
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def failed_with(exception)
|
|
19
|
-
FailedExecution.
|
|
19
|
+
FailedExecution.transaction(requires_new: true) do
|
|
20
|
+
FailedExecution.create!(job_id: id, exception: exception)
|
|
21
|
+
end
|
|
22
|
+
rescue ActiveRecord::RecordNotUnique
|
|
23
|
+
if (failed_execution = FailedExecution.find_by(job_id: id))
|
|
24
|
+
failed_execution.exception = exception
|
|
25
|
+
failed_execution.save!
|
|
26
|
+
else
|
|
27
|
+
retry
|
|
28
|
+
end
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def reset_execution_counters
|
|
@@ -30,7 +30,8 @@ module SolidQueue
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def select_candidates(queue_relation, limit)
|
|
33
|
-
|
|
33
|
+
# Force query execution here with #to_a to avoid unintended FOR UPDATE query executions
|
|
34
|
+
queue_relation.ordered.limit(limit).non_blocking_lock.select(:id, :job_id).to_a
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def lock_candidates(executions, process_id)
|
|
@@ -20,6 +20,13 @@ module SolidQueue
|
|
|
20
20
|
connection.supports_insert_conflict_target?
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
# Pass index hints to the query optimizer using SQL comment hints.
|
|
25
|
+
# Uses MySQL 8 optimizer hint query comments, which SQLite and
|
|
26
|
+
# PostgreSQL ignore.
|
|
27
|
+
def use_index(*indexes)
|
|
28
|
+
optimizer_hints "INDEX(#{quoted_table_name} #{indexes.join(', ')})"
|
|
29
|
+
end
|
|
23
30
|
end
|
|
24
31
|
end
|
|
25
32
|
end
|
|
@@ -6,11 +6,12 @@ module SolidQueue
|
|
|
6
6
|
class RecurringTask < Record
|
|
7
7
|
serialize :arguments, coder: Arguments, default: []
|
|
8
8
|
|
|
9
|
-
validate :
|
|
9
|
+
validate :ensure_schedule_supported
|
|
10
10
|
validate :ensure_command_or_class_present
|
|
11
|
-
validate :
|
|
11
|
+
validate :ensure_existing_job_class
|
|
12
12
|
|
|
13
13
|
scope :static, -> { where(static: true) }
|
|
14
|
+
scope :dynamic, -> { where(static: false) }
|
|
14
15
|
|
|
15
16
|
has_many :recurring_executions, foreign_key: :task_key, primary_key: :key
|
|
16
17
|
|
|
@@ -32,7 +33,15 @@ module SolidQueue
|
|
|
32
33
|
queue_name: options[:queue].presence,
|
|
33
34
|
priority: options[:priority].presence,
|
|
34
35
|
description: options[:description],
|
|
35
|
-
static: true
|
|
36
|
+
static: options.fetch(:static, true)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_dynamic_task(key, **options)
|
|
40
|
+
from_configuration(key, **options.merge(static: false)).save!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def delete_dynamic_task(key)
|
|
44
|
+
RecurringTask.dynamic.find_by!(key: key).destroy
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
def create_or_update_all(tasks)
|
|
@@ -102,19 +111,28 @@ module SolidQueue
|
|
|
102
111
|
end
|
|
103
112
|
|
|
104
113
|
private
|
|
105
|
-
def
|
|
114
|
+
def ensure_schedule_supported
|
|
106
115
|
unless parsed_schedule.instance_of?(Fugit::Cron)
|
|
107
116
|
errors.add :schedule, :unsupported, message: "is not a supported recurring schedule"
|
|
108
117
|
end
|
|
118
|
+
rescue ArgumentError => error
|
|
119
|
+
message = if error.message.include?("multiple crons")
|
|
120
|
+
"generates multiple cron schedules. Please use separate recurring tasks for each schedule, " +
|
|
121
|
+
"or use explicit cron syntax (e.g., '40 0,15 * * *' for multiple times with the same minutes)"
|
|
122
|
+
else
|
|
123
|
+
error.message
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
errors.add :schedule, :unsupported, message: message
|
|
109
127
|
end
|
|
110
128
|
|
|
111
129
|
def ensure_command_or_class_present
|
|
112
130
|
unless command.present? || class_name.present?
|
|
113
|
-
errors.add :base, :command_and_class_blank, message: "either command or
|
|
131
|
+
errors.add :base, :command_and_class_blank, message: "either command or class must be present"
|
|
114
132
|
end
|
|
115
133
|
end
|
|
116
134
|
|
|
117
|
-
def
|
|
135
|
+
def ensure_existing_job_class
|
|
118
136
|
if class_name.present? && job_class.nil?
|
|
119
137
|
errors.add :class_name, :undefined, message: "doesn't correspond to an existing class"
|
|
120
138
|
end
|
|
@@ -152,7 +170,7 @@ module SolidQueue
|
|
|
152
170
|
|
|
153
171
|
|
|
154
172
|
def parsed_schedule
|
|
155
|
-
@parsed_schedule ||= Fugit.parse(schedule)
|
|
173
|
+
@parsed_schedule ||= Fugit.parse(schedule, multi: :fail)
|
|
156
174
|
end
|
|
157
175
|
|
|
158
176
|
def job_class
|
|
@@ -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,38 +15,78 @@ 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
|
|
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
|
|
19
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
|
-
|
|
84
|
+
def stop_solid_queue_fork
|
|
85
|
+
return unless solid_queue_pid
|
|
86
|
+
|
|
39
87
|
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
|
40
88
|
log "Stopping Solid Queue..."
|
|
41
|
-
Process.kill(:INT, solid_queue_pid)
|
|
89
|
+
Process.kill(:INT, solid_queue_pid)
|
|
42
90
|
Process.wait(solid_queue_pid)
|
|
43
91
|
rescue Errno::ECHILD, Errno::ESRCH
|
|
44
92
|
end
|
|
@@ -48,7 +96,7 @@ Puma::Plugin.create do
|
|
|
48
96
|
end
|
|
49
97
|
|
|
50
98
|
def monitor_solid_queue
|
|
51
|
-
monitor(:
|
|
99
|
+
monitor(:solid_queue_fork_dead?, "Detected Solid Queue has gone away, stopping Puma...")
|
|
52
100
|
end
|
|
53
101
|
|
|
54
102
|
def monitor(process_dead, message)
|
|
@@ -62,7 +110,7 @@ Puma::Plugin.create do
|
|
|
62
110
|
end
|
|
63
111
|
end
|
|
64
112
|
|
|
65
|
-
def
|
|
113
|
+
def solid_queue_fork_dead?
|
|
66
114
|
if solid_queue_started?
|
|
67
115
|
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
|
68
116
|
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,52 @@
|
|
|
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, _| replace_thread(thread_id) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def replace_thread(thread_id)
|
|
26
|
+
SolidQueue.instrument(:replace_thread, supervisor_pid: ::Process.pid) do |payload|
|
|
27
|
+
if (instance = process_instances.delete(thread_id))
|
|
28
|
+
payload[:thread] = instance
|
|
29
|
+
|
|
30
|
+
error = Processes::ThreadTerminatedError.new(instance.name)
|
|
31
|
+
release_claimed_jobs_by(instance, with_error: error)
|
|
32
|
+
|
|
33
|
+
start_process(configured_processes.delete(thread_id))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def perform_graceful_termination
|
|
39
|
+
process_instances.values.each(&:stop)
|
|
40
|
+
|
|
41
|
+
Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_processes_terminated? })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def perform_immediate_termination
|
|
45
|
+
exit!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def all_processes_terminated?
|
|
49
|
+
process_instances.values.none?(&:alive?)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
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, 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"
|