solid_queue 1.4.0 → 1.5.1
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 +30 -4
- data/UPGRADING.md +11 -0
- data/app/models/solid_queue/claimed_execution.rb +23 -14
- data/app/models/solid_queue/job/executable.rb +6 -5
- data/app/models/solid_queue/job/schedulable.rb +2 -1
- data/app/models/solid_queue/queue.rb +1 -3
- data/app/models/solid_queue/queue_selector.rb +2 -2
- data/app/models/solid_queue/record/distinct_values.rb +50 -0
- data/app/models/solid_queue/record.rb +13 -0
- data/app/models/solid_queue/recurring_task.rb +30 -4
- data/app/models/solid_queue/semaphore.rb +7 -1
- data/lib/active_job/queue_adapters/solid_queue_adapter.rb +9 -1
- data/lib/generators/solid_queue/install/templates/config/queue.yml +1 -1
- data/lib/generators/solid_queue/update/update_generator.rb +19 -0
- data/lib/solid_queue/cli.rb +10 -0
- data/lib/solid_queue/configuration.rb +53 -23
- data/lib/solid_queue/engine.rb +10 -0
- data/lib/solid_queue/fork_supervisor.rb +23 -3
- data/lib/solid_queue/log_subscriber.rb +5 -0
- data/lib/solid_queue/processes/runnable.rb +90 -3
- data/lib/solid_queue/scheduler/recurring_schedule.rb +7 -5
- data/lib/solid_queue/supervisor/signals.rb +1 -1
- data/lib/solid_queue/supervisor.rb +2 -0
- data/lib/solid_queue/tasks.rb +11 -0
- data/lib/solid_queue/timer.rb +3 -4
- data/lib/solid_queue/version.rb +5 -1
- data/lib/solid_queue.rb +14 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82eb2e9041fa69b3e7f3bd4a3d62c46b7826100a708b2237750f0f9e4fdee9c5
|
|
4
|
+
data.tar.gz: 14000eeba6030e0248273d4cc158e17f25f78268d5e2c62c5e74f9a959f8d94f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9bb7b225b9a3b1b13e1f2edfa04927cb2f3954d0a0b226ca79e7fc69dff6675c002ac5e6dd13fd0ff9794d2f0a660487a70cb078868034e7a048378b14911a4f
|
|
7
|
+
data.tar.gz: b30ef8f4992fc7a3d858294b3ae9b1c4459f813d9ec3bb3ff4a4fde97ad114469d76d913bb19912117de3d081e6c8d159798e5bcdd0790858ea3af6f4faf7cc6
|
data/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Solid Queue can be used with SQL databases such as MySQL, PostgreSQL, or SQLite,
|
|
|
23
23
|
- [Threads, processes, and signals](#threads-processes-and-signals)
|
|
24
24
|
- [Database configuration](#database-configuration)
|
|
25
25
|
- [Other configuration settings](#other-configuration-settings)
|
|
26
|
+
- [Validating the configuration](#validating-the-configuration)
|
|
26
27
|
- [Lifecycle hooks](#lifecycle-hooks)
|
|
27
28
|
- [Errors when enqueuing](#errors-when-enqueuing)
|
|
28
29
|
- [Concurrency controls](#concurrency-controls)
|
|
@@ -44,7 +45,7 @@ Solid Queue is configured by default in new Rails 8 applications. If you're runn
|
|
|
44
45
|
1. `bundle add solid_queue`
|
|
45
46
|
2. `bin/rails solid_queue:install`
|
|
46
47
|
|
|
47
|
-
(Note: The minimum supported version of Rails is 7.1 and Ruby is 3.
|
|
48
|
+
(Note: The minimum supported version of Rails is 7.1 and Ruby is 3.2.)
|
|
48
49
|
|
|
49
50
|
This will configure Solid Queue as the production Active Job backend, create the configuration files `config/queue.yml` and `config/recurring.yml`, and create the `db/queue_schema.rb`. It'll also create a `bin/jobs` executable wrapper that you can use to start Solid Queue.
|
|
50
51
|
|
|
@@ -213,6 +214,8 @@ bin/jobs -c config/calendar.yml
|
|
|
213
214
|
|
|
214
215
|
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`.
|
|
215
216
|
|
|
217
|
+
To run **only** the scheduler (no workers or dispatchers)—for example to isolate recurring tasks on a dedicated process—set `SOLID_QUEUE_ONLY_RECURRING=true` or use the `--only-recurring` option with `bin/jobs`.
|
|
218
|
+
|
|
216
219
|
This is what this configuration looks like:
|
|
217
220
|
|
|
218
221
|
```yml
|
|
@@ -338,7 +341,7 @@ FROM solid_queue_ready_executions
|
|
|
338
341
|
WHERE queue_name LIKE 'beta%';
|
|
339
342
|
```
|
|
340
343
|
|
|
341
|
-
This type of `DISTINCT` query on a column that's the leftmost column in an index can be performed very fast in MySQL thanks to a technique called [Loose Index Scan](https://dev.mysql.com/doc/refman/8.0/en/group-by-optimization.html#loose-index-scan). PostgreSQL
|
|
344
|
+
This type of `DISTINCT` query on a column that's the leftmost column in an index can be performed very fast in MySQL thanks to a technique called [Loose Index Scan](https://dev.mysql.com/doc/refman/8.0/en/group-by-optimization.html#loose-index-scan). PostgreSQL doesn't implement this technique natively, so Solid Queue uses a [recursive CTE](https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-RECURSIVE) to emulate it, achieving similar performance by walking the B-tree index and jumping between distinct values. SQLite doesn't implement loose index scan either, but this is unlikely to be a problem since SQLite is typically used in development with small datasets.
|
|
342
345
|
|
|
343
346
|
Similarly to using prefixes, the same will happen if you have paused queues, because we need to get a list of all queues with a query like
|
|
344
347
|
```sql
|
|
@@ -372,6 +375,8 @@ The supervisor is in charge of managing these processes, and it responds to the
|
|
|
372
375
|
|
|
373
376
|
When receiving a `QUIT` signal, if workers still have jobs in-flight, these will be returned to the queue when the processes are deregistered.
|
|
374
377
|
|
|
378
|
+
On Windows, the `QUIT` signal can't be trapped, so the supervisor only responds to `TERM` and `INT` there.
|
|
379
|
+
|
|
375
380
|
If processes have no chance of cleaning up before exiting (e.g. if someone pulls a cable somewhere), in-flight jobs might remain claimed by the processes executing them. Processes send heartbeats, and the supervisor checks and prunes processes with expired heartbeats. Jobs that were claimed by processes with an expired heartbeat will be marked as failed with a `SolidQueue::Processes::ProcessPrunedError`. You can configure both the frequency of heartbeats and the threshold to consider a process dead. See the section below for this.
|
|
376
381
|
|
|
377
382
|
In a similar way, if a worker is terminated in any other way not initiated by the above signals (e.g. a worker is sent a `KILL` signal), jobs in progress will be marked as failed so that they can be inspected, with a `SolidQueue::Processes::ProcessExitError`. Sometimes a job in particular is responsible for this, for example, if it has a memory leak and you have a mechanism to kill processes over a certain memory threshold, so this will help identifying this kind of situation.
|
|
@@ -401,6 +406,7 @@ There are several settings that control how Solid Queue works that you can set a
|
|
|
401
406
|
- `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.
|
|
402
407
|
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
|
403
408
|
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
|
409
|
+
- `fork_boot_timeout`: how long a forked process can take to finish booting before the supervisor replaces it—defaults to 5 minutes. It only applies in the default `fork` mode.
|
|
404
410
|
- `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.
|
|
405
411
|
- `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `true`.
|
|
406
412
|
- `supervisor_pidfile`: path to a pidfile that the supervisor will create when booting to prevent running more than one supervisor in the same host, or in case you want to use it for a health check. It's `nil` by default.
|
|
@@ -408,6 +414,22 @@ There are several settings that control how Solid Queue works that you can set a
|
|
|
408
414
|
- `clear_finished_jobs_after`: period to keep finished jobs around, in case `preserve_finished_jobs` is true — defaults to 1 day. When installing Solid Queue, [a recurring job](#recurring-tasks) is automatically configured to clear finished jobs every hour on the 12th minute in batches. You can edit the `recurring.yml` configuration to change this as you see fit.
|
|
409
415
|
- `default_concurrency_control_period`: the value to be used as the default for the `duration` parameter in [concurrency controls](#concurrency-controls). It defaults to 3 minutes.
|
|
410
416
|
|
|
417
|
+
### Validating the configuration
|
|
418
|
+
|
|
419
|
+
You can validate the Solid Queue configuration ahead of time, without starting any process. This is handy in deploy scripts or CI to catch mistakes—a typo in `recurring.yml`, no processes configured, and so on—before they cause a supervisor to boot into a broken state:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# Using the bin/jobs binstub
|
|
423
|
+
bin/jobs check
|
|
424
|
+
|
|
425
|
+
# Or via rake
|
|
426
|
+
bin/rails solid_queue:check
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
Both commands validate the configuration for the current Rails environment. On success they print `Solid Queue configuration is valid.` and exit `0`; otherwise they print the errors and exit non-zero. When the number of threads is larger than the [database connection pool](#database-configuration), they also print an advisory warning about it—the same one the supervisor logs on boot. They're tolerant of a missing database connection, so they can run on CI or deploy hosts without database credentials.
|
|
430
|
+
|
|
431
|
+
`bin/jobs check` accepts the same options as `bin/jobs start` (e.g. `--config_file`, `--recurring_schedule_file`, `--skip-recurring`). The rake task honors the same environment variables Solid Queue already uses: `SOLID_QUEUE_CONFIG`, `SOLID_QUEUE_RECURRING_SCHEDULE`, and `SOLID_QUEUE_SKIP_RECURRING`. To validate a specific environment's configuration, set `RAILS_ENV`, for example `RAILS_ENV=production bin/jobs check`.
|
|
432
|
+
|
|
411
433
|
|
|
412
434
|
## Lifecycle hooks
|
|
413
435
|
|
|
@@ -471,7 +493,7 @@ Solid Queue extends Active Job with concurrency controls, that allows you to lim
|
|
|
471
493
|
|
|
472
494
|
```ruby
|
|
473
495
|
class MyJob < ApplicationJob
|
|
474
|
-
limits_concurrency to: max_concurrent_executions, key: ->(arg1, arg2,
|
|
496
|
+
limits_concurrency to: max_concurrent_executions, key: ->(arg1, arg2, *) { ... }, duration: max_interval_to_guarantee_concurrency_limit, group: concurrency_group, on_conflict: on_conflict_behaviour
|
|
475
497
|
|
|
476
498
|
# ...
|
|
477
499
|
```
|
|
@@ -690,6 +712,8 @@ bin/jobs --recurring_schedule_file=config/schedule.yml
|
|
|
690
712
|
|
|
691
713
|
You can completely disable recurring tasks by setting the environment variable `SOLID_QUEUE_SKIP_RECURRING=true` or by using the `--skip-recurring` option with `bin/jobs`.
|
|
692
714
|
|
|
715
|
+
To run only the scheduler (no workers or dispatchers), set `SOLID_QUEUE_ONLY_RECURRING=true` or use `--only-recurring` with `bin/jobs`.
|
|
716
|
+
|
|
693
717
|
The configuration itself looks like this:
|
|
694
718
|
|
|
695
719
|
```yml
|
|
@@ -705,7 +729,9 @@ production:
|
|
|
705
729
|
|
|
706
730
|
Tasks are specified as a hash/dictionary, where the key will be the task's key internally. Each task needs to either have a `class`, which will be the job class to enqueue, or a `command`, which will be eval'ed in the context of a job (`SolidQueue::RecurringJob`) that will be enqueued according to its schedule, in the `solid_queue_recurring` queue.
|
|
707
731
|
|
|
708
|
-
Each task needs to have also a schedule, which is parsed using [Fugit](https://github.com/floraison/fugit), so it accepts anything [that Fugit accepts as a cron](https://github.com/floraison/fugit?tab=readme-ov-file#fugitcron). You can
|
|
732
|
+
Each task needs to have also a schedule, which is parsed using [Fugit](https://github.com/floraison/fugit), so it accepts anything [that Fugit accepts as a cron](https://github.com/floraison/fugit?tab=readme-ov-file#fugitcron). Schedules can include a time zone (e.g. `0 9 * * * America/New_York` or `every day at 9am America/New_York`). When a schedule doesn't specify one, it's interpreted in the application's configured time zone (`config.time_zone`) by default. You can change or disable this default with `config.solid_queue.time_zone`; setting it to `nil` falls back to the system's local time.
|
|
733
|
+
|
|
734
|
+
You can optionally supply the following for each task:
|
|
709
735
|
- `args`: the arguments to be passed to the job, as a single argument, a hash, or an array of arguments that can also include kwargs as the last element in the array.
|
|
710
736
|
|
|
711
737
|
The job in the example configuration above will be enqueued every second as:
|
data/UPGRADING.md
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# Upgrading to version 1.5.x
|
|
2
|
+
Ruby 3.1 is no longer supported, as it reached end-of-life in March 2025. Solid Queue now requires Ruby 3.2 or newer. If you're still on Ruby 3.1, Bundler will continue to resolve solid_queue 1.4.x for you, but you won't receive any new versions until you upgrade Ruby.
|
|
3
|
+
|
|
4
|
+
Recurring schedules that don't specify a time zone are now interpreted in your application's configured time zone (`config.time_zone`) by default, instead of the system's local time. This only affects schedules without an explicit time zone (e.g. `every day at 9am`); schedules that already include one (e.g. `0 9 * * * America/New_York`) are unaffected.
|
|
5
|
+
|
|
6
|
+
If your `config.time_zone` differs from the system time where your processes run, recurring jobs may fire at a different wall-clock time than before. To keep the previous behavior, set:
|
|
7
|
+
|
|
8
|
+
```ruby
|
|
9
|
+
config.solid_queue.time_zone = nil
|
|
10
|
+
```
|
|
11
|
+
|
|
1
12
|
# Upgrading to version 1.x
|
|
2
13
|
The value returned for `enqueue_after_transaction_commit?` has changed to `true`, and it's no longer configurable. If you want to change this, you need to use Active Job's configuration options.
|
|
3
14
|
|
|
@@ -43,7 +43,6 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
|
|
|
43
43
|
SolidQueue.instrument(:fail_many_claimed) do |payload|
|
|
44
44
|
executions.each do |execution|
|
|
45
45
|
execution.failed_with(error)
|
|
46
|
-
execution.unblock_next_job
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
payload[:process_ids] = executions.map(&:process_id).uniq
|
|
@@ -71,13 +70,11 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
|
|
|
71
70
|
failed_with(result.error)
|
|
72
71
|
raise result.error
|
|
73
72
|
end
|
|
74
|
-
ensure
|
|
75
|
-
unblock_next_job
|
|
76
73
|
end
|
|
77
74
|
|
|
78
75
|
def release
|
|
79
76
|
SolidQueue.instrument(:release_claimed, job_id: job.id, process_id: process_id) do
|
|
80
|
-
|
|
77
|
+
unless_already_finalized do
|
|
81
78
|
job.dispatch_bypassing_concurrency_limits
|
|
82
79
|
destroy!
|
|
83
80
|
end
|
|
@@ -89,14 +86,7 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
|
|
|
89
86
|
end
|
|
90
87
|
|
|
91
88
|
def failed_with(error)
|
|
92
|
-
|
|
93
|
-
job.failed_with(error)
|
|
94
|
-
destroy!
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def unblock_next_job
|
|
99
|
-
job.unblock_next_blocked_job
|
|
89
|
+
finalize { job.failed_with(error) }
|
|
100
90
|
end
|
|
101
91
|
|
|
102
92
|
private
|
|
@@ -108,9 +98,28 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
|
|
|
108
98
|
end
|
|
109
99
|
|
|
110
100
|
def finished
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
finalize { job.finished! }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def finalize
|
|
105
|
+
finalized = unless_already_finalized do
|
|
106
|
+
yield
|
|
113
107
|
destroy!
|
|
108
|
+
true
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Unblock the next job outside the finalize transaction so a failure while
|
|
112
|
+
# releasing the concurrency lock or dispatching the next job can't roll back
|
|
113
|
+
# a job that already finished or failed. Only the actor that owned and
|
|
114
|
+
# finalized the claim gets here, so the lock is released exactly once.
|
|
115
|
+
job.unblock_next_blocked_job if finalized
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def unless_already_finalized
|
|
119
|
+
transaction do
|
|
120
|
+
return false unless self.class.unscoped.lock.find_by(id: id)
|
|
121
|
+
|
|
122
|
+
yield
|
|
114
123
|
end
|
|
115
124
|
end
|
|
116
125
|
end
|
|
@@ -41,15 +41,16 @@ module SolidQueue
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def successfully_dispatched(jobs)
|
|
44
|
-
|
|
44
|
+
jobs_by_id = jobs.index_by(&:id)
|
|
45
|
+
dispatched_and_ready(jobs_by_id) + dispatched_and_blocked(jobs_by_id)
|
|
45
46
|
end
|
|
46
47
|
|
|
47
|
-
def dispatched_and_ready(
|
|
48
|
-
|
|
48
|
+
def dispatched_and_ready(jobs_by_id)
|
|
49
|
+
ReadyExecution.where(job_id: jobs_by_id.keys).pluck(:job_id).map { |id| jobs_by_id[id] }
|
|
49
50
|
end
|
|
50
51
|
|
|
51
|
-
def dispatched_and_blocked(
|
|
52
|
-
|
|
52
|
+
def dispatched_and_blocked(jobs_by_id)
|
|
53
|
+
BlockedExecution.where(job_id: jobs_by_id.keys).pluck(:job_id).map { |id| jobs_by_id[id] }
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -23,7 +23,8 @@ module SolidQueue
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def successfully_scheduled(jobs)
|
|
26
|
-
|
|
26
|
+
jobs_by_id = jobs.index_by(&:id)
|
|
27
|
+
ScheduledExecution.where(job_id: jobs_by_id.keys).pluck(:job_id).map { |id| jobs_by_id[id] }
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -43,7 +43,7 @@ module SolidQueue
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def all_queues
|
|
46
|
-
relation.
|
|
46
|
+
relation.distinct_values_of(:queue_name)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def exact_names
|
|
@@ -53,7 +53,7 @@ module SolidQueue
|
|
|
53
53
|
def prefixed_names
|
|
54
54
|
if prefixes.empty? then []
|
|
55
55
|
else
|
|
56
|
-
relation.where(([ "queue_name LIKE ?" ] * prefixes.count).join(" OR "), *prefixes).
|
|
56
|
+
relation.where(([ "queue_name LIKE ?" ] * prefixes.count).join(" OR "), *prefixes).distinct_values_of(:queue_name)
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class Record
|
|
5
|
+
module DistinctValues
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
# PostgreSQL has no native loose index scan, so a plain DISTINCT on a leading
|
|
9
|
+
# index column degrades to a full index scan on large tables. We emulate one
|
|
10
|
+
# with a recursive CTE that walks the index jumping between distinct values.
|
|
11
|
+
class_methods do
|
|
12
|
+
def distinct_values_of(column)
|
|
13
|
+
if loose_index_scan_emulation_needed?
|
|
14
|
+
loose_distinct_via_recursive_cte(column)
|
|
15
|
+
else
|
|
16
|
+
distinct.pluck(column)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
def loose_index_scan_emulation_needed?
|
|
22
|
+
connection_pool.with_connection { |connection| connection.adapter_name == "PostgreSQL" }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Emulates a loose index scan, honoring the current scope (e.g. LIKE prefixes)
|
|
26
|
+
# by building the anchor and the recursive step as scoped relations, whose
|
|
27
|
+
# #to_sql inlines any bind parameters so they can be embedded in the raw CTE.
|
|
28
|
+
def loose_distinct_via_recursive_cte(column)
|
|
29
|
+
connection_pool.with_connection do |connection|
|
|
30
|
+
col = connection.quote_column_name(column)
|
|
31
|
+
|
|
32
|
+
connection.select_values(<<~SQL.squish)
|
|
33
|
+
WITH RECURSIVE t AS (
|
|
34
|
+
(#{next_distinct_value(col, "#{col} IS NOT NULL")})
|
|
35
|
+
UNION ALL
|
|
36
|
+
SELECT (#{next_distinct_value(col, "#{col} > t.#{col}")}) FROM t WHERE t.#{col} IS NOT NULL
|
|
37
|
+
)
|
|
38
|
+
SELECT #{col} FROM t WHERE #{col} IS NOT NULL
|
|
39
|
+
SQL
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Smallest value of `col` within the current scope that matches `condition`.
|
|
44
|
+
def next_distinct_value(col, condition)
|
|
45
|
+
all.where(Arel.sql(condition)).reorder(Arel.sql(col)).limit(1).select(Arel.sql(col)).to_sql
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
module SolidQueue
|
|
4
4
|
class Record < ActiveRecord::Base
|
|
5
5
|
self.abstract_class = true
|
|
6
|
+
self.strict_loading_by_default = false
|
|
7
|
+
|
|
8
|
+
include DistinctValues
|
|
6
9
|
|
|
7
10
|
connects_to(**SolidQueue.connects_to) if SolidQueue.connects_to
|
|
8
11
|
|
|
@@ -21,6 +24,16 @@ module SolidQueue
|
|
|
21
24
|
end
|
|
22
25
|
end
|
|
23
26
|
|
|
27
|
+
def warn_about_pending_migrations
|
|
28
|
+
SolidQueue.deprecator.warn(<<~DEPRECATION)
|
|
29
|
+
Solid Queue has pending database migrations. To get the new migration files, run:
|
|
30
|
+
rails solid_queue:update
|
|
31
|
+
And then:
|
|
32
|
+
rails db:migrate
|
|
33
|
+
These migrations will be required after version #{SolidQueue.next_major_version}.0
|
|
34
|
+
DEPRECATION
|
|
35
|
+
end
|
|
36
|
+
|
|
24
37
|
# Pass index hints to the query optimizer using SQL comment hints.
|
|
25
38
|
# Uses MySQL 8 optimizer hint query comments, which SQLite and
|
|
26
39
|
# PostgreSQL ignore.
|
|
@@ -56,16 +56,17 @@ module SolidQueue
|
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
|
|
60
|
+
def next_time_after(time)
|
|
61
|
+
parsed_schedule_with_time_zone.next_time(time).utc
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
def next_time
|
|
64
|
-
|
|
65
|
+
parsed_schedule_with_time_zone.next_time.utc
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def previous_time
|
|
68
|
-
|
|
69
|
+
parsed_schedule_with_time_zone.previous_time.utc
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def last_enqueued_time
|
|
@@ -85,6 +86,7 @@ module SolidQueue
|
|
|
85
86
|
|
|
86
87
|
perform_later.tap do |job|
|
|
87
88
|
unless job.successfully_enqueued?
|
|
89
|
+
report_enqueue_error(job.enqueue_error, at: at)
|
|
88
90
|
payload[:enqueue_error] = job.enqueue_error&.message
|
|
89
91
|
end
|
|
90
92
|
end
|
|
@@ -97,6 +99,7 @@ module SolidQueue
|
|
|
97
99
|
payload[:skipped] = true
|
|
98
100
|
false
|
|
99
101
|
rescue Job::EnqueueError => error
|
|
102
|
+
report_enqueue_error(error, at: at)
|
|
100
103
|
payload[:enqueue_error] = error.message
|
|
101
104
|
false
|
|
102
105
|
end
|
|
@@ -168,11 +171,24 @@ module SolidQueue
|
|
|
168
171
|
end
|
|
169
172
|
end
|
|
170
173
|
|
|
174
|
+
def parsed_schedule_with_time_zone
|
|
175
|
+
@parsed_schedule_with_time_zone ||= apply_default_time_zone_to(parsed_schedule)
|
|
176
|
+
end
|
|
171
177
|
|
|
172
178
|
def parsed_schedule
|
|
173
179
|
@parsed_schedule ||= Fugit.parse(schedule, multi: :fail)
|
|
174
180
|
end
|
|
175
181
|
|
|
182
|
+
def apply_default_time_zone_to(schedule)
|
|
183
|
+
if schedule.respond_to?(:zone) && schedule.zone.nil? && default_time_zone.present?
|
|
184
|
+
Fugit.parse("#{schedule.to_cron_s} #{default_time_zone}", multi: :fail)
|
|
185
|
+
else
|
|
186
|
+
schedule
|
|
187
|
+
end
|
|
188
|
+
rescue ArgumentError
|
|
189
|
+
schedule
|
|
190
|
+
end
|
|
191
|
+
|
|
176
192
|
def job_class
|
|
177
193
|
@job_class ||= class_name.present? ? class_name.safe_constantize : self.class.default_job_class
|
|
178
194
|
end
|
|
@@ -180,5 +196,15 @@ module SolidQueue
|
|
|
180
196
|
def enqueue_options
|
|
181
197
|
{ queue: queue_name, priority: priority }.compact
|
|
182
198
|
end
|
|
199
|
+
|
|
200
|
+
def default_time_zone
|
|
201
|
+
SolidQueue.time_zone
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def report_enqueue_error(error, at:)
|
|
205
|
+
if error
|
|
206
|
+
Rails.error.report(error, handled: true, source: "application.solid_queue", context: { task: key, at: at })
|
|
207
|
+
end
|
|
208
|
+
end
|
|
183
209
|
end
|
|
184
210
|
end
|
|
@@ -32,7 +32,13 @@ module SolidQueue
|
|
|
32
32
|
|
|
33
33
|
class Proxy
|
|
34
34
|
def self.signal_all(jobs)
|
|
35
|
-
|
|
35
|
+
# Guard against incrementing a semaphore's value beyond its limit. Jobs can
|
|
36
|
+
# have different limits, so group them and cap each group with `value < limit`.
|
|
37
|
+
jobs.group_by { |job| job.concurrency_limit || 1 }.each do |limit, grouped_jobs|
|
|
38
|
+
Semaphore.where(key: grouped_jobs.map(&:concurrency_key))
|
|
39
|
+
.where(value: ...limit)
|
|
40
|
+
.update_all("value = value + 1")
|
|
41
|
+
end
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def initialize(job)
|
|
@@ -8,9 +8,17 @@ module ActiveJob
|
|
|
8
8
|
#
|
|
9
9
|
# Rails.application.config.active_job.queue_adapter = :solid_queue
|
|
10
10
|
class SolidQueueAdapter < (Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1 ? Object : AbstractAdapter)
|
|
11
|
-
class_attribute :stopping, default: false, instance_writer: false
|
|
11
|
+
class_attribute :stopping, default: false, instance_writer: false, instance_predicate: false
|
|
12
12
|
SolidQueue.on_worker_stop { self.stopping = true }
|
|
13
13
|
|
|
14
|
+
# Accept an optional job argument for compatibility with Rails main, which
|
|
15
|
+
# began passing the running job to +queue_adapter.stopping?+ so adapters can
|
|
16
|
+
# decide whether to checkpoint based on it. We rely solely on the worker
|
|
17
|
+
# shutdown flag, so the argument is ignored.
|
|
18
|
+
def stopping?(_job = nil)
|
|
19
|
+
self.class.stopping
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
def enqueue_after_transaction_commit?
|
|
15
23
|
true
|
|
16
24
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/active_record"
|
|
4
|
+
|
|
5
|
+
class SolidQueue::UpdateGenerator < Rails::Generators::Base
|
|
6
|
+
include ActiveRecord::Generators::Migration
|
|
7
|
+
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
class_option :database, type: :string, aliases: %i[ --db ], default: "queue",
|
|
11
|
+
desc: "The database that Solid Queue uses. Defaults to `queue`"
|
|
12
|
+
|
|
13
|
+
def copy_new_migrations
|
|
14
|
+
Dir.glob(File.join(self.class.source_root, "db", "*.rb")).each do |migration_file|
|
|
15
|
+
name = File.basename(migration_file)
|
|
16
|
+
migration_template File.join("db", name), File.join(db_migrate_path, name), skip: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/solid_queue/cli.rb
CHANGED
|
@@ -20,6 +20,10 @@ module SolidQueue
|
|
|
20
20
|
desc: "Whether to skip recurring tasks scheduling",
|
|
21
21
|
banner: "SOLID_QUEUE_SKIP_RECURRING"
|
|
22
22
|
|
|
23
|
+
class_option :only_recurring, type: :boolean,
|
|
24
|
+
desc: "Whether to run only the scheduler process for recurring tasks",
|
|
25
|
+
banner: "SOLID_QUEUE_ONLY_RECURRING"
|
|
26
|
+
|
|
23
27
|
def self.exit_on_failure?
|
|
24
28
|
true
|
|
25
29
|
end
|
|
@@ -30,5 +34,11 @@ module SolidQueue
|
|
|
30
34
|
def start
|
|
31
35
|
SolidQueue::Supervisor.start(**options.symbolize_keys)
|
|
32
36
|
end
|
|
37
|
+
|
|
38
|
+
desc :check, "Validates the Solid Queue configuration for the current Rails env without starting anything. Exits non-zero on errors."
|
|
39
|
+
def check
|
|
40
|
+
configuration = SolidQueue::Configuration.new(**options.symbolize_keys)
|
|
41
|
+
exit 1 unless configuration.check
|
|
42
|
+
end
|
|
33
43
|
end
|
|
34
44
|
end
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
module SolidQueue
|
|
4
4
|
class Configuration
|
|
5
5
|
include ActiveModel::Model
|
|
6
|
+
include ActiveModel::Validations::Callbacks
|
|
6
7
|
|
|
7
|
-
validate :ensure_configured_processes
|
|
8
|
-
validate :
|
|
9
|
-
|
|
8
|
+
validate :ensure_configured_processes, :ensure_valid_recurring_tasks
|
|
9
|
+
validate :warn_about_incorrectly_sized_thread_pool, :warn_about_missing_config_files
|
|
10
|
+
|
|
11
|
+
before_validation { warnings.clear }
|
|
10
12
|
|
|
11
13
|
class Process < Struct.new(:kind, :attributes)
|
|
12
14
|
def instantiate
|
|
@@ -41,32 +43,41 @@ module SolidQueue
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
def configured_processes
|
|
44
|
-
if only_work?
|
|
46
|
+
if only_work?
|
|
47
|
+
workers
|
|
48
|
+
elsif only_recurring?
|
|
49
|
+
schedulers
|
|
45
50
|
else
|
|
46
51
|
dispatchers + workers + schedulers
|
|
47
52
|
end
|
|
48
53
|
end
|
|
49
54
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
else
|
|
54
|
-
error_messages = invalid_tasks.map do |task|
|
|
55
|
-
all_messages = task.errors.full_messages.map { |msg| "\t#{msg}" }.join("\n")
|
|
56
|
-
"#{task.key}:\n#{all_messages}"
|
|
57
|
-
end
|
|
58
|
-
.join("\n")
|
|
55
|
+
def mode
|
|
56
|
+
options[:mode].to_s.inquiry
|
|
57
|
+
end
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
def standalone?
|
|
60
|
+
mode.fork? || options[:standalone]
|
|
62
61
|
end
|
|
63
62
|
|
|
64
|
-
def
|
|
65
|
-
@
|
|
63
|
+
def warnings
|
|
64
|
+
@warnings ||= ActiveModel::Errors.new(self)
|
|
66
65
|
end
|
|
67
66
|
|
|
68
|
-
def
|
|
69
|
-
|
|
67
|
+
def check
|
|
68
|
+
if valid?
|
|
69
|
+
warnings.full_messages.each { |warning| $stderr.puts warning }
|
|
70
|
+
$stdout.puts "Solid Queue configuration is valid."
|
|
71
|
+
|
|
72
|
+
true
|
|
73
|
+
else
|
|
74
|
+
$stderr.puts "Solid Queue configuration is invalid:"
|
|
75
|
+
(warnings.full_messages + errors.full_messages).each do |message|
|
|
76
|
+
message.each_line { |line| $stderr.puts " #{line.chomp}" }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
false
|
|
80
|
+
end
|
|
70
81
|
end
|
|
71
82
|
|
|
72
83
|
private
|
|
@@ -88,11 +99,26 @@ module SolidQueue
|
|
|
88
99
|
end
|
|
89
100
|
end
|
|
90
101
|
|
|
91
|
-
def
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
def warn_about_incorrectly_sized_thread_pool
|
|
103
|
+
db_pool_size = SolidQueue::Record.connection_pool&.size
|
|
104
|
+
|
|
105
|
+
if db_pool_size && db_pool_size < estimated_number_of_threads
|
|
106
|
+
warnings.add(:base, "Warning: Solid Queue is configured to use #{estimated_number_of_threads} threads but the " \
|
|
94
107
|
"database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
|
|
95
108
|
end
|
|
109
|
+
rescue ActiveRecord::ActiveRecordError
|
|
110
|
+
# No usable database connection. Skip the pool-size warning in that case.
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def warn_about_missing_config_files
|
|
114
|
+
files = [ options[:config_file] ]
|
|
115
|
+
files << options[:recurring_schedule_file] unless skip_recurring_tasks?
|
|
116
|
+
|
|
117
|
+
files.compact.each do |file|
|
|
118
|
+
unless Pathname.new(file).exist?
|
|
119
|
+
warnings.add(:base, "Warning: provided configuration file '#{file}' does not exist. Falling back to default configuration.")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
96
122
|
end
|
|
97
123
|
|
|
98
124
|
def default_options
|
|
@@ -103,6 +129,7 @@ module SolidQueue
|
|
|
103
129
|
recurring_schedule_file: Rails.root.join(ENV["SOLID_QUEUE_RECURRING_SCHEDULE"] || DEFAULT_RECURRING_SCHEDULE_FILE_PATH),
|
|
104
130
|
only_work: false,
|
|
105
131
|
only_dispatch: false,
|
|
132
|
+
only_recurring: ActiveModel::Type::Boolean.new.cast(ENV["SOLID_QUEUE_ONLY_RECURRING"]),
|
|
106
133
|
skip_recurring: ActiveModel::Type::Boolean.new.cast(ENV["SOLID_QUEUE_SKIP_RECURRING"])
|
|
107
134
|
}
|
|
108
135
|
end
|
|
@@ -119,6 +146,10 @@ module SolidQueue
|
|
|
119
146
|
options[:only_dispatch]
|
|
120
147
|
end
|
|
121
148
|
|
|
149
|
+
def only_recurring?
|
|
150
|
+
options[:only_recurring]
|
|
151
|
+
end
|
|
152
|
+
|
|
122
153
|
def skip_recurring_tasks?
|
|
123
154
|
options[:skip_recurring] || only_work?
|
|
124
155
|
end
|
|
@@ -221,7 +252,6 @@ module SolidQueue
|
|
|
221
252
|
if file.exist?
|
|
222
253
|
ActiveSupport::ConfigurationFile.parse(file).deep_symbolize_keys
|
|
223
254
|
else
|
|
224
|
-
puts "[solid_queue] WARNING: Provided configuration file '#{file}' does not exist. Falling back to default configuration."
|
|
225
255
|
{}
|
|
226
256
|
end
|
|
227
257
|
end
|
data/lib/solid_queue/engine.rb
CHANGED
|
@@ -16,6 +16,12 @@ module SolidQueue
|
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
initializer "solid_queue.time_zone" do |app|
|
|
20
|
+
unless config.solid_queue.key?(:time_zone)
|
|
21
|
+
SolidQueue.time_zone = app.config.time_zone
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
19
25
|
initializer "solid_queue.app_executor", before: :run_prepare_callbacks do |app|
|
|
20
26
|
config.solid_queue.app_executor ||= app.executor
|
|
21
27
|
config.solid_queue.on_thread_error ||= ->(exception) { Rails.error.report(exception, handled: false) }
|
|
@@ -37,5 +43,9 @@ module SolidQueue
|
|
|
37
43
|
include ActiveJob::ConcurrencyControls
|
|
38
44
|
end
|
|
39
45
|
end
|
|
46
|
+
|
|
47
|
+
initializer "solid_queue.deprecator" do |app|
|
|
48
|
+
app.deprecators[:solid_queue] = SolidQueue.deprecator
|
|
49
|
+
end
|
|
40
50
|
end
|
|
41
51
|
end
|
|
@@ -31,6 +31,21 @@ module SolidQueue
|
|
|
31
31
|
|
|
32
32
|
replace_fork(pid, status)
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
check_boot_timeouts
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_boot_timeouts
|
|
39
|
+
process_instances.each do |pid, instance|
|
|
40
|
+
terminate_unready_process(pid) if instance.boot_timed_out?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def terminate_unready_process(pid)
|
|
45
|
+
SolidQueue.instrument(:fork_boot_timeout, process: process_instances[pid], pid: pid) do
|
|
46
|
+
# A child stuck in boot cannot reach its run loop to stop gracefully
|
|
47
|
+
signal_process(pid, :KILL)
|
|
48
|
+
end
|
|
34
49
|
end
|
|
35
50
|
|
|
36
51
|
def reap_terminated_forks
|
|
@@ -38,9 +53,13 @@ module SolidQueue
|
|
|
38
53
|
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
39
54
|
break unless pid
|
|
40
55
|
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
if terminated_fork = process_instances.delete(pid)
|
|
57
|
+
terminated_fork.mark_as_reaped
|
|
58
|
+
|
|
59
|
+
if !status.exited? || status.exitstatus.to_i > 0
|
|
60
|
+
error = Processes::ProcessExitError.new(status)
|
|
61
|
+
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
62
|
+
end
|
|
44
63
|
end
|
|
45
64
|
|
|
46
65
|
configured_processes.delete(pid)
|
|
@@ -52,6 +71,7 @@ module SolidQueue
|
|
|
52
71
|
def replace_fork(pid, status)
|
|
53
72
|
SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
|
|
54
73
|
if terminated_fork = process_instances.delete(pid)
|
|
74
|
+
terminated_fork.mark_as_reaped
|
|
55
75
|
payload[:fork] = terminated_fork
|
|
56
76
|
error = Processes::ProcessExitError.new(status)
|
|
57
77
|
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
@@ -161,6 +161,11 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
+
def fork_boot_timeout(event)
|
|
165
|
+
process = event.payload[:process]
|
|
166
|
+
warn formatted_event(event, action: "Terminate #{process.kind} that failed to boot in time", **event.payload.slice(:pid).merge(hostname: process.hostname, name: process.name))
|
|
167
|
+
end
|
|
168
|
+
|
|
164
169
|
private
|
|
165
170
|
def formatted_event(event, action:, **attributes)
|
|
166
171
|
"SolidQueue-#{SolidQueue::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
|
|
@@ -4,7 +4,9 @@ module SolidQueue::Processes
|
|
|
4
4
|
module Runnable
|
|
5
5
|
include Supervised
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
def mode=(value)
|
|
8
|
+
@mode = (value || DEFAULT_MODE).to_s.inquiry
|
|
9
|
+
end
|
|
8
10
|
|
|
9
11
|
def start
|
|
10
12
|
run_in_mode do
|
|
@@ -29,21 +31,32 @@ module SolidQueue::Processes
|
|
|
29
31
|
!running_async? || @thread&.alive?
|
|
30
32
|
end
|
|
31
33
|
|
|
34
|
+
def boot_timed_out?
|
|
35
|
+
@boot_guard.timed_out?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark_as_reaped
|
|
39
|
+
@boot_guard.close
|
|
40
|
+
end
|
|
41
|
+
|
|
32
42
|
private
|
|
33
43
|
DEFAULT_MODE = :async
|
|
34
44
|
|
|
35
45
|
def mode
|
|
36
|
-
|
|
46
|
+
@mode ||= DEFAULT_MODE.to_s.inquiry
|
|
37
47
|
end
|
|
38
48
|
|
|
39
49
|
def run_in_mode(&block)
|
|
40
50
|
case
|
|
41
51
|
when running_as_fork?
|
|
42
|
-
|
|
52
|
+
@boot_guard = BootGuards::ForkGuard.new
|
|
53
|
+
fork(&block).tap { @boot_guard.start }
|
|
43
54
|
when running_async?
|
|
55
|
+
@boot_guard = BootGuards::NullGuard.new
|
|
44
56
|
@thread = create_thread(&block)
|
|
45
57
|
@thread.object_id
|
|
46
58
|
else
|
|
59
|
+
@boot_guard = BootGuards::NullGuard.new
|
|
47
60
|
block.call
|
|
48
61
|
end
|
|
49
62
|
end
|
|
@@ -57,6 +70,8 @@ module SolidQueue::Processes
|
|
|
57
70
|
end
|
|
58
71
|
end
|
|
59
72
|
end
|
|
73
|
+
|
|
74
|
+
@boot_guard.complete
|
|
60
75
|
end
|
|
61
76
|
|
|
62
77
|
def shutting_down?
|
|
@@ -93,4 +108,76 @@ module SolidQueue::Processes
|
|
|
93
108
|
mode.fork?
|
|
94
109
|
end
|
|
95
110
|
end
|
|
111
|
+
|
|
112
|
+
module BootGuards
|
|
113
|
+
# Tracks a process that shares memory with its supervisor, whose boot time
|
|
114
|
+
# doesn't need monitoring.
|
|
115
|
+
class NullGuard
|
|
116
|
+
def complete
|
|
117
|
+
@completed = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def start
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def completed?
|
|
124
|
+
@completed
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def timed_out?
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def close
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Tracks a forked process from the moment it's started until its boot
|
|
136
|
+
# callbacks finish, over a pipe that survives forking: the forked process
|
|
137
|
+
# writes to it when it's done booting, and its supervisor reads from it to
|
|
138
|
+
# decide whether the process is taking too long to boot and needs replacing.
|
|
139
|
+
class ForkGuard
|
|
140
|
+
def initialize
|
|
141
|
+
@reader, @writer = IO.pipe
|
|
142
|
+
@created_at = SolidQueue::Timer.monotonic_time_now
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Runs in the forked process when it has finished booting
|
|
146
|
+
def complete
|
|
147
|
+
reader.close
|
|
148
|
+
writer.write(".")
|
|
149
|
+
rescue Errno::EPIPE
|
|
150
|
+
# The supervisor stopped waiting while this process finished booting
|
|
151
|
+
ensure
|
|
152
|
+
writer.close
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Runs in the parent process right after forking
|
|
156
|
+
def start
|
|
157
|
+
writer.close
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A byte means boot completed; EOF means the process exited before
|
|
161
|
+
# finishing its boot, and will be replaced when it's reaped
|
|
162
|
+
def completed?
|
|
163
|
+
@completed ||= begin
|
|
164
|
+
completed = reader.read_nonblock(1, exception: false) != :wait_readable
|
|
165
|
+
reader.close if completed
|
|
166
|
+
completed
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def timed_out?
|
|
171
|
+
!completed? && SolidQueue::Timer.monotonic_time_now - created_at >= SolidQueue.fork_boot_timeout
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def close
|
|
175
|
+
reader.close unless reader.closed?
|
|
176
|
+
writer.close unless writer.closed?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
attr_reader :reader, :writer, :created_at
|
|
181
|
+
end
|
|
182
|
+
end
|
|
96
183
|
end
|
|
@@ -33,8 +33,8 @@ module SolidQueue
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def schedule_task(task)
|
|
37
|
-
scheduled_tasks[task.key] = schedule(task)
|
|
36
|
+
def schedule_task(task, run_at: task.next_time)
|
|
37
|
+
scheduled_tasks[task.key] = schedule(task, run_at: run_at)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def unschedule_tasks
|
|
@@ -99,9 +99,11 @@ module SolidQueue
|
|
|
99
99
|
dynamic_tasks_enabled? ? RecurringTask.dynamic.to_a : []
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
def schedule(task)
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
def schedule(task, run_at: task.next_time)
|
|
103
|
+
delay = [ (run_at - Time.current).to_f, 0.1 ].max
|
|
104
|
+
|
|
105
|
+
scheduled_task = Concurrent::ScheduledTask.new(delay, args: [ self, task, run_at ]) do |thread_schedule, thread_task, thread_task_run_at|
|
|
106
|
+
thread_schedule.schedule_task(thread_task, run_at: thread_task.next_time_after(thread_task_run_at))
|
|
105
107
|
|
|
106
108
|
wrap_in_app_executor do
|
|
107
109
|
thread_task.enqueue(at: thread_task_run_at)
|
|
@@ -13,6 +13,8 @@ module SolidQueue
|
|
|
13
13
|
configuration = Configuration.new(**options)
|
|
14
14
|
|
|
15
15
|
if configuration.valid?
|
|
16
|
+
configuration.warnings.full_messages.each { |warning| SolidQueue.logger.warn(warning) }
|
|
17
|
+
|
|
16
18
|
klass = configuration.mode.fork? ? ForkSupervisor : AsyncSupervisor
|
|
17
19
|
klass.new(configuration).tap(&:start)
|
|
18
20
|
else
|
data/lib/solid_queue/tasks.rb
CHANGED
|
@@ -4,8 +4,19 @@ namespace :solid_queue do
|
|
|
4
4
|
Rails::Command.invoke :generate, [ "solid_queue:install" ]
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
desc "Copy any new Solid Queue migrations to the application"
|
|
8
|
+
task :update do
|
|
9
|
+
Rails::Command.invoke :generate, [ "solid_queue:update" ]
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
desc "start solid_queue supervisor to dispatch and process jobs"
|
|
8
13
|
task start: :environment do
|
|
9
14
|
SolidQueue::Supervisor.start
|
|
10
15
|
end
|
|
16
|
+
|
|
17
|
+
desc "validate the Solid Queue configuration for the current Rails env without starting any process"
|
|
18
|
+
task check: :environment do
|
|
19
|
+
configuration = SolidQueue::Configuration.new
|
|
20
|
+
exit 1 unless configuration.check
|
|
21
|
+
end
|
|
11
22
|
end
|
data/lib/solid_queue/timer.rb
CHANGED
data/lib/solid_queue/version.rb
CHANGED
data/lib/solid_queue.rb
CHANGED
|
@@ -29,6 +29,7 @@ module SolidQueue
|
|
|
29
29
|
|
|
30
30
|
mattr_accessor :process_heartbeat_interval, default: 60.seconds
|
|
31
31
|
mattr_accessor :process_alive_threshold, default: 5.minutes
|
|
32
|
+
mattr_accessor :fork_boot_timeout, default: 5.minutes
|
|
32
33
|
|
|
33
34
|
mattr_accessor :shutdown_timeout, default: 5.seconds
|
|
34
35
|
|
|
@@ -41,6 +42,15 @@ module SolidQueue
|
|
|
41
42
|
mattr_accessor :clear_finished_jobs_after, default: 1.day
|
|
42
43
|
mattr_accessor :default_concurrency_control_period, default: 3.minutes
|
|
43
44
|
|
|
45
|
+
mattr_reader :time_zone
|
|
46
|
+
|
|
47
|
+
def time_zone=(zone)
|
|
48
|
+
@@time_zone = if zone
|
|
49
|
+
resolved = zone.respond_to?(:tzinfo) ? zone : ActiveSupport::TimeZone[zone]
|
|
50
|
+
resolved&.tzinfo&.name || zone.to_s
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
44
54
|
delegate :on_start, :on_stop, :on_exit, to: Supervisor
|
|
45
55
|
|
|
46
56
|
def schedule_recurring_task(key, **options)
|
|
@@ -77,6 +87,10 @@ module SolidQueue
|
|
|
77
87
|
preserve_finished_jobs
|
|
78
88
|
end
|
|
79
89
|
|
|
90
|
+
def deprecator
|
|
91
|
+
@deprecator ||= ActiveSupport::Deprecation.new(next_major_version, "SolidQueue")
|
|
92
|
+
end
|
|
93
|
+
|
|
80
94
|
def instrument(channel, **options, &block)
|
|
81
95
|
ActiveSupport::Notifications.instrument("#{channel}.solid_queue", **options, &block)
|
|
82
96
|
end
|
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.5.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rosa Gutierrez
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -294,6 +294,7 @@ files:
|
|
|
294
294
|
- app/models/solid_queue/queue_selector.rb
|
|
295
295
|
- app/models/solid_queue/ready_execution.rb
|
|
296
296
|
- app/models/solid_queue/record.rb
|
|
297
|
+
- app/models/solid_queue/record/distinct_values.rb
|
|
297
298
|
- app/models/solid_queue/recurring_execution.rb
|
|
298
299
|
- app/models/solid_queue/recurring_task.rb
|
|
299
300
|
- app/models/solid_queue/recurring_task/arguments.rb
|
|
@@ -308,6 +309,7 @@ files:
|
|
|
308
309
|
- lib/generators/solid_queue/install/templates/config/queue.yml
|
|
309
310
|
- lib/generators/solid_queue/install/templates/config/recurring.yml
|
|
310
311
|
- lib/generators/solid_queue/install/templates/db/queue_schema.rb
|
|
312
|
+
- lib/generators/solid_queue/update/update_generator.rb
|
|
311
313
|
- lib/puma/plugin/solid_queue.rb
|
|
312
314
|
- lib/solid_queue.rb
|
|
313
315
|
- lib/solid_queue/app_executor.rb
|
|
@@ -360,7 +362,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
360
362
|
requirements:
|
|
361
363
|
- - ">="
|
|
362
364
|
- !ruby/object:Gem::Version
|
|
363
|
-
version: '3.
|
|
365
|
+
version: '3.2'
|
|
364
366
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
365
367
|
requirements:
|
|
366
368
|
- - ">="
|