solid_queue 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +94 -59
- data/app/models/solid_queue/blocked_execution.rb +2 -2
- data/app/models/solid_queue/claimed_execution.rb +0 -5
- data/app/models/solid_queue/execution/job_attributes.rb +12 -5
- data/app/models/solid_queue/execution.rb +12 -0
- data/app/models/solid_queue/failed_execution.rb +1 -5
- data/app/models/solid_queue/job/executable.rb +42 -14
- data/app/models/solid_queue/job/schedulable.rb +40 -0
- data/app/models/solid_queue/job.rb +47 -27
- data/app/models/solid_queue/process/prunable.rb +1 -1
- data/app/models/solid_queue/ready_execution.rb +2 -2
- data/app/models/solid_queue/record.rb +3 -3
- data/app/models/solid_queue/scheduled_execution.rb +3 -35
- data/db/migrate/20231211200639_create_solid_queue_tables.rb +1 -1
- data/lib/active_job/concurrency_controls.rb +4 -0
- data/lib/active_job/queue_adapters/solid_queue_adapter.rb +6 -6
- data/lib/generators/solid_queue/install/install_generator.rb +6 -4
- data/lib/generators/solid_queue/install/templates/config.yml +18 -0
- data/lib/puma/plugin/solid_queue.rb +10 -7
- data/lib/solid_queue/configuration.rb +22 -14
- data/lib/solid_queue/processes/runnable.rb +3 -1
- data/lib/solid_queue/supervisor.rb +2 -2
- data/lib/solid_queue/version.rb +1 -1
- metadata +23 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 72d9dc2e30127c7b1fe101648d4d18b04af2d33491c729816e25dcf488246b54
|
4
|
+
data.tar.gz: e39dcad251d4124355adcedfa9cb1ee88eea12043eb8505568332d9fcacee428
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 967732a5e679269543869ca64aa410bfc43c935ad3fb6856de0860bbe5cee58716e389b5935e48a91c6c658a84fa79aff9f5eae93ba3931344d31aff0d45c8fc
|
7
|
+
data.tar.gz: 523af9f1b26a137d99ade0805bdc955cb81584971308928d5e16e83d3c46861947deaaa95ff96775d7eceb913f3d8995eca14707cda4533f7c3ee39f20afb650
|
data/README.md
CHANGED
@@ -2,27 +2,11 @@
|
|
2
2
|
|
3
3
|
Solid Queue is a DB-based queuing backend for [Active Job](https://edgeguides.rubyonrails.org/active_job_basics.html), designed with simplicity and performance in mind.
|
4
4
|
|
5
|
-
Solid Queue
|
5
|
+
Besides regular job enqueuing and processing, Solid Queue supports delayed jobs, concurrency controls, pausing queues, numeric priorities per job, priorities by queue order, and bulk enqueuing (`enqueue_all` for Active Job's `perform_all_later`). _Improvements to logging and instrumentation, a better CLI tool, a way to run within an existing process in "async" mode, unique jobs and recurring, cron-like tasks are coming very soon._
|
6
6
|
|
7
|
-
|
8
|
-
To set Solid Queue as your Active Job's queue backend, you should add this to your environment config:
|
9
|
-
```ruby
|
10
|
-
# config/environments/production.rb
|
11
|
-
config.active_job.queue_adapter = :solid_queue
|
12
|
-
```
|
13
|
-
|
14
|
-
Alternatively, you can set only specific jobs to use Solid Queue as their backend if you're migrating from another adapter and want to move jobs progressively:
|
15
|
-
|
16
|
-
```ruby
|
17
|
-
# app/jobs/my_job.rb
|
18
|
-
|
19
|
-
class MyJob < ApplicationJob
|
20
|
-
self.queue_adapter = :solid_queue
|
21
|
-
# ...
|
22
|
-
end
|
23
|
-
```
|
7
|
+
Solid Queue can be used with SQL databases such as MySQL, PostgreSQL or SQLite, and it leverages the `FOR UPDATE SKIP LOCKED` clause, if available, to avoid blocking and waiting on locks when polling jobs. It relies on Active Job for retries, discarding, error handling, serialization, or delays, and it's compatible with Ruby on Rails multi-threading.
|
24
8
|
|
25
|
-
## Installation
|
9
|
+
## Installation and usage
|
26
10
|
Add this line to your application's Gemfile:
|
27
11
|
|
28
12
|
```ruby
|
@@ -39,21 +23,51 @@ Or install it yourself as:
|
|
39
23
|
$ gem install solid_queue
|
40
24
|
```
|
41
25
|
|
42
|
-
|
26
|
+
Now, you need to install the necessary migrations and configure the Active Job's adapter. You can do both at once using the provided generator:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
$ bin/rails generate solid_queue:install
|
43
30
|
```
|
31
|
+
|
32
|
+
This will set `solid_queue` as the Active Job's adapter in production, and will copy the required migration over to your app.
|
33
|
+
|
34
|
+
Alternatively, you can add the only the migration to your app:
|
35
|
+
```bash
|
44
36
|
$ bin/rails solid_queue:install:migrations
|
45
|
-
$ bin/rails db:migrate
|
46
37
|
```
|
47
38
|
|
48
|
-
|
39
|
+
And set Solid Queue as your Active Job's queue backend manually, in your environment config:
|
40
|
+
```ruby
|
41
|
+
# config/environments/production.rb
|
42
|
+
config.active_job.queue_adapter = :solid_queue
|
43
|
+
```
|
44
|
+
|
45
|
+
Alternatively, you can set only specific jobs to use Solid Queue as their backend if you're migrating from another adapter and want to move jobs progressively:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# app/jobs/my_job.rb
|
49
|
+
|
50
|
+
class MyJob < ApplicationJob
|
51
|
+
self.queue_adapter = :solid_queue
|
52
|
+
# ...
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
Finally, you need to run the migrations:
|
57
|
+
|
58
|
+
```bash
|
59
|
+
$ bin/rails db:migrate
|
49
60
|
```
|
61
|
+
|
62
|
+
After this, you'll be ready to enqueue jobs using Solid Queue, but you need to start Solid Queue's supervisor to run them.
|
63
|
+
```bash
|
50
64
|
$ bundle exec rake solid_queue:start
|
51
65
|
```
|
52
66
|
|
53
67
|
This will start processing jobs in all queues using the default configuration. See [below](#configuration) to learn more about configuring Solid Queue.
|
54
68
|
|
55
69
|
## Requirements
|
56
|
-
Besides Rails 7, Solid Queue works best with MySQL 8+ 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.
|
70
|
+
Besides Rails 7.1, Solid Queue works best with MySQL 8+ 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.
|
57
71
|
|
58
72
|
## Configuration
|
59
73
|
|
@@ -61,7 +75,7 @@ Besides Rails 7, Solid Queue works best with MySQL 8+ or PostgreSQL 9.5+, as the
|
|
61
75
|
|
62
76
|
We have three types of processes in Solid Queue:
|
63
77
|
- _Workers_ are in charge of picking jobs ready to run from queues and processing them. They work off the `solid_queue_ready_executions` table.
|
64
|
-
- _Dispatchers_ are in charge of selecting jobs scheduled to run in the future that are due and _dispatching_ them, which is simply moving them from the `
|
78
|
+
- _Dispatchers_ are in charge of selecting jobs scheduled to run in the future that are due and _dispatching_ them, which is simply moving them from the `solid_queue_scheduled_executions` table over to the `solid_queue_ready_executions` table so that workers can pick them up. They also do some maintenance work related to concurrency controls.
|
65
79
|
- The _supervisor_ forks workers and dispatchers according to the configuration, controls their heartbeats, and sends them signals to stop and start them when needed.
|
66
80
|
|
67
81
|
By default, Solid Queue will try to find your configuration under `config/solid_queue.yml`, but you can set a different path using the environment variable `SOLID_QUEUE_CONFIG`. This is what this configuration looks like:
|
@@ -71,11 +85,12 @@ production:
|
|
71
85
|
dispatchers:
|
72
86
|
- polling_interval: 1
|
73
87
|
batch_size: 500
|
88
|
+
concurrency_maintenance_interval: 300
|
74
89
|
workers:
|
75
|
-
- queues: *
|
90
|
+
- queues: "*"
|
76
91
|
threads: 3
|
77
92
|
polling_interval: 2
|
78
|
-
- queues: real_time,background
|
93
|
+
- queues: [ real_time, background ]
|
79
94
|
threads: 5
|
80
95
|
polling_interval: 0.1
|
81
96
|
processes: 3
|
@@ -83,20 +98,23 @@ production:
|
|
83
98
|
|
84
99
|
Everything is optional. If no configuration is provided, Solid Queue will run with one dispatcher and one worker with default settings.
|
85
100
|
|
86
|
-
- `polling_interval`: the time interval in seconds that workers and dispatchers will wait before checking for more jobs. This time defaults to `
|
87
|
-
- `batch_size`: the dispatcher will dispatch jobs in batches of this size.
|
88
|
-
- `
|
89
|
-
|
90
|
-
staging:
|
91
|
-
workers:
|
92
|
-
- queues: staging*
|
93
|
-
threads: 3
|
94
|
-
polling_interval: 5
|
101
|
+
- `polling_interval`: the time interval in seconds that workers and dispatchers will wait before checking for more jobs. This time defaults to `1` second for dispatchers and `0.1` seconds for workers.
|
102
|
+
- `batch_size`: the dispatcher will dispatch jobs in batches of this size. The default is 500.
|
103
|
+
- `concurrency_maintenance_interval`: the time interval in seconds that the dispatcher will wait before checking for blocked jobs that can be unblocked. Read more about [concurrency controls](#concurrency-controls) to learn more about this setting. It defaults to `600` seconds.
|
104
|
+
- `queues`: the list of queues that workers will pick jobs from. You can use `*` to indicate all queues (which is also the default and the behaviour you'll get if you omit this). You can provide a single queue, or a list of queues as an array. Jobs will be polled from those queues in order, so for example, with `[ real_time, background ]`, no jobs will be taken from `background` unless there aren't any more jobs waiting in `real_time`. You can also provide a prefix with a wildcard to match queues starting with a prefix. For example:
|
95
105
|
|
96
|
-
```
|
97
|
-
|
106
|
+
```yml
|
107
|
+
staging:
|
108
|
+
workers:
|
109
|
+
- queues: staging*
|
110
|
+
threads: 3
|
111
|
+
polling_interval: 5
|
112
|
+
|
113
|
+
```
|
98
114
|
|
99
|
-
|
115
|
+
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.
|
116
|
+
|
117
|
+
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.
|
100
118
|
- `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 `5`. Only workers have this setting.
|
101
119
|
- `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.
|
102
120
|
|
@@ -129,23 +147,25 @@ There are several settings that control how Solid Queue works that you can set a
|
|
129
147
|
- `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
|
130
148
|
- `app_executor`: the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
|
131
149
|
- `on_thread_error`: custom lambda/Proc to call when there's an error within a thread that takes the exception raised as argument. Defaults to
|
132
|
-
|
133
|
-
|
134
|
-
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
-> (exception) { Rails.error.report(exception, handled: false) }
|
153
|
+
```
|
135
154
|
- `connects_to`: a custom database configuration that will be used in the abstract `SolidQueue::Record` Active Record model. This is required to use a different database than the main app. For example:
|
136
|
-
|
155
|
+
|
156
|
+
```ruby
|
137
157
|
# Use a separate DB for Solid Queue
|
138
158
|
config.solid_queue.connects_to = { database: { writing: :solid_queue_primary, reading: :solid_queue_replica } }
|
139
|
-
```
|
159
|
+
```
|
140
160
|
- `use_skip_locked`: whether to use `FOR UPDATE SKIP LOCKED` when performing locking reads. This will be automatically detected in the future, and for now, you'd only need to set this to `false` if your database doesn't support it. For MySQL, that'd be versions < 8, and for PostgreSQL, versions < 9.5. If you use SQLite, this has no effect, as writes are sequential.
|
141
|
-
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to
|
142
|
-
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to
|
143
|
-
- `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
|
144
|
-
- `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to
|
161
|
+
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
162
|
+
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
163
|
+
- `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.
|
164
|
+
- `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `false`.
|
145
165
|
- `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.
|
146
|
-
- `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to
|
147
|
-
- `clear_finished_jobs_after`: period to keep finished jobs around, in case `preserve_finished_jobs` is true—defaults to
|
148
|
-
- `default_concurrency_control_period`: the value to be used as the default for the `duration` parameter in [concurrency controls](#concurrency-controls). It defaults to
|
166
|
+
- `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`.
|
167
|
+
- `clear_finished_jobs_after`: period to keep finished jobs around, in case `preserve_finished_jobs` is true—defaults to 1 day. **Note:** Right now, there's no automatic cleanup of finished jobs. You'd need to do this by periodically invoking `SolidQueue::Job.clear_finished_in_batches`, but this will happen automatically in the near future.
|
168
|
+
- `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.
|
149
169
|
|
150
170
|
|
151
171
|
## Concurrency controls
|
@@ -197,6 +217,20 @@ Note that the `duration` setting depends indirectly on the value for `concurrenc
|
|
197
217
|
|
198
218
|
Finally, failed jobs that are automatically or manually retried work in the same way as new jobs that get enqueued: they get in the queue for gaining the lock, and whenever they get it, they'll be run. It doesn't matter if they had gained the lock already in the past.
|
199
219
|
|
220
|
+
## Failed jobs and retries
|
221
|
+
|
222
|
+
Solid Queue doesn't include any automatic retry mechanism, it [relies on Active Job for this](https://edgeguides.rubyonrails.org/active_job_basics.html#retrying-or-discarding-failed-jobs). Jobs that fail will be kept in the system, and a _failed execution_ (a record in the `solid_queue_failed_executions` table) will be created for these. The job will stay there until manually discarded or re-enqueued. You can do this in a console as:
|
223
|
+
```ruby
|
224
|
+
failed_execution = SolidQueue::FailedExecution.find(...) # Find the failed execution related to your job
|
225
|
+
failed_execution.error # inspect the error
|
226
|
+
|
227
|
+
failed_execution.retry # This will re-enqueue the job as if it was enqueued for the first time
|
228
|
+
failed_execution.discard # This will delete the job from the system
|
229
|
+
```
|
230
|
+
|
231
|
+
We're planning to release a dashboard called _Mission Control_, where, among other things, you'll be able to examine and retry/discard failed jobs, one by one, or in bulk.
|
232
|
+
|
233
|
+
|
200
234
|
## Puma plugin
|
201
235
|
We provide a Puma plugin if you want to run the Solid Queue's supervisor together with Puma and have Puma monitor and manage it. You just need to add
|
202
236
|
```ruby
|
@@ -211,20 +245,21 @@ to your `puma.rb` configuration.
|
|
211
245
|
If you prefer not to rely on this, or avoid relying on it unintentionally, you should make sure that:
|
212
246
|
- Your jobs relying on specific records are always enqueued on [`after_commit` callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#after-commit-and-after-rollback) or otherwise from a place where you're certain that whatever data the job will use has been committed to the database before the job is enqueued.
|
213
247
|
- Or, to opt out completely from this behaviour, configure a database for Solid Queue, even if it's the same as your app, ensuring that a different connection on the thread handling requests or running jobs for your app will be used to enqueue jobs. For example:
|
214
|
-
```ruby
|
215
|
-
class ApplicationRecord < ActiveRecord::Base
|
216
|
-
self.abstract_class = true
|
217
248
|
|
218
|
-
|
219
|
-
|
249
|
+
```ruby
|
250
|
+
class ApplicationRecord < ActiveRecord::Base
|
251
|
+
self.abstract_class = true
|
220
252
|
|
221
|
-
|
222
|
-
|
223
|
-
|
253
|
+
connects_to database: { writing: :primary, reading: :replica }
|
254
|
+
```
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
config.solid_queue.connects_to = { database: { writing: :primary, reading: :replica } }
|
258
|
+
```
|
224
259
|
|
225
260
|
## Inspiration
|
226
261
|
|
227
|
-
Solid Queue has been inspired by [resque](https://github.com/resque/resque) and [GoodJob](https://github.com/bensheldon/good_job).
|
262
|
+
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.
|
228
263
|
|
229
264
|
## License
|
230
265
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module SolidQueue
|
4
4
|
class BlockedExecution < Execution
|
5
|
-
|
5
|
+
assumes_attributes_from_job :concurrency_key
|
6
6
|
before_create :set_expires_at
|
7
7
|
|
8
8
|
has_one :semaphore, foreign_key: :key, primary_key: :concurrency_key
|
@@ -24,7 +24,7 @@ module SolidQueue
|
|
24
24
|
|
25
25
|
def release_one(concurrency_key)
|
26
26
|
transaction do
|
27
|
-
ordered.where(concurrency_key: concurrency_key).limit(1).
|
27
|
+
ordered.where(concurrency_key: concurrency_key).limit(1).non_blocking_lock.each(&:release)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -46,7 +46,6 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
|
|
46
46
|
|
47
47
|
private
|
48
48
|
def execute
|
49
|
-
SolidQueue.logger.info("[SolidQueue] Performing job #{job.id} - #{job.active_job_id}")
|
50
49
|
ActiveJob::Base.execute(job.arguments)
|
51
50
|
Result.new(true, nil)
|
52
51
|
rescue Exception => e
|
@@ -58,8 +57,6 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
|
|
58
57
|
job.finished!
|
59
58
|
destroy!
|
60
59
|
end
|
61
|
-
|
62
|
-
SolidQueue.logger.info("[SolidQueue] Performed job #{job.id} - #{job.active_job_id}")
|
63
60
|
end
|
64
61
|
|
65
62
|
def failed_with(error)
|
@@ -67,7 +64,5 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
|
|
67
64
|
job.failed_with(error)
|
68
65
|
destroy!
|
69
66
|
end
|
70
|
-
|
71
|
-
SolidQueue.logger.info("[SolidQueue] Failed job #{job.id} - #{job.active_job_id}")
|
72
67
|
end
|
73
68
|
end
|
@@ -5,17 +5,24 @@ module SolidQueue
|
|
5
5
|
module JobAttributes
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
-
|
8
|
+
included do
|
9
|
+
class_attribute :assumible_attributes_from_job, instance_accessor: false, default: %i[ queue_name priority ]
|
10
|
+
end
|
9
11
|
|
10
12
|
class_methods do
|
11
|
-
def
|
12
|
-
|
13
|
+
def assumes_attributes_from_job(*attribute_names)
|
14
|
+
self.assumible_attributes_from_job |= attribute_names
|
15
|
+
before_create -> { assume_attributes_from_job }
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes_from_job(job)
|
19
|
+
job.attributes.symbolize_keys.slice(*assumible_attributes_from_job)
|
13
20
|
end
|
14
21
|
end
|
15
22
|
|
16
23
|
private
|
17
|
-
def assume_attributes_from_job
|
18
|
-
|
24
|
+
def assume_attributes_from_job
|
25
|
+
self.class.assumible_attributes_from_job.each do |attribute|
|
19
26
|
send("#{attribute}=", job.send(attribute))
|
20
27
|
end
|
21
28
|
end
|
@@ -11,5 +11,17 @@ module SolidQueue
|
|
11
11
|
belongs_to :job
|
12
12
|
|
13
13
|
alias_method :discard, :destroy
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def create_all_from_jobs(jobs)
|
17
|
+
insert_all execution_data_from_jobs(jobs)
|
18
|
+
end
|
19
|
+
|
20
|
+
def execution_data_from_jobs(jobs)
|
21
|
+
jobs.collect do |job|
|
22
|
+
attributes_from_job(job).merge(job_id: job.id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
14
26
|
end
|
15
27
|
end
|
@@ -1,11 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class SolidQueue::FailedExecution < SolidQueue::Execution
|
4
|
-
|
5
|
-
serialize :error, coder: JSON
|
6
|
-
else
|
7
|
-
serialize :error, JSON
|
8
|
-
end
|
4
|
+
serialize :error, coder: JSON
|
9
5
|
|
10
6
|
before_create :expand_error_details_from_exception
|
11
7
|
|
@@ -6,20 +6,56 @@ module SolidQueue
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
included do
|
9
|
-
include Clearable, ConcurrencyControls
|
9
|
+
include Clearable, ConcurrencyControls, Schedulable
|
10
10
|
|
11
11
|
has_one :ready_execution, dependent: :destroy
|
12
12
|
has_one :claimed_execution, dependent: :destroy
|
13
13
|
has_one :failed_execution, dependent: :destroy
|
14
14
|
|
15
|
-
has_one :scheduled_execution, dependent: :destroy
|
16
|
-
|
17
15
|
after_create :prepare_for_execution
|
18
16
|
|
19
17
|
scope :finished, -> { where.not(finished_at: nil) }
|
18
|
+
scope :failed, -> { includes(:failed_execution).where.not(failed_execution: { id: nil }) }
|
20
19
|
end
|
21
20
|
|
22
|
-
|
21
|
+
class_methods do
|
22
|
+
def prepare_all_for_execution(jobs)
|
23
|
+
due, not_yet_due = jobs.partition(&:due?)
|
24
|
+
dispatch_all(due) + schedule_all(not_yet_due)
|
25
|
+
end
|
26
|
+
|
27
|
+
def dispatch_all(jobs)
|
28
|
+
with_concurrency_limits, without_concurrency_limits = jobs.partition(&:concurrency_limited?)
|
29
|
+
|
30
|
+
dispatch_all_at_once(without_concurrency_limits)
|
31
|
+
dispatch_all_one_by_one(with_concurrency_limits)
|
32
|
+
|
33
|
+
successfully_dispatched(jobs)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
def dispatch_all_at_once(jobs)
|
38
|
+
ReadyExecution.create_all_from_jobs jobs
|
39
|
+
end
|
40
|
+
|
41
|
+
def dispatch_all_one_by_one(jobs)
|
42
|
+
jobs.each(&:dispatch)
|
43
|
+
end
|
44
|
+
|
45
|
+
def successfully_dispatched(jobs)
|
46
|
+
dispatched_and_ready(jobs) + dispatched_and_blocked(jobs)
|
47
|
+
end
|
48
|
+
|
49
|
+
def dispatched_and_ready(jobs)
|
50
|
+
where(id: ReadyExecution.where(job_id: jobs.map(&:id)).pluck(:job_id))
|
51
|
+
end
|
52
|
+
|
53
|
+
def dispatched_and_blocked(jobs)
|
54
|
+
where(id: BlockedExecution.where(job_id: jobs.map(&:id)).pluck(:job_id))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
%w[ ready claimed failed ].each do |status|
|
23
59
|
define_method("#{status}?") { public_send("#{status}_execution").present? }
|
24
60
|
end
|
25
61
|
|
@@ -49,8 +85,8 @@ module SolidQueue
|
|
49
85
|
finished_at.present?
|
50
86
|
end
|
51
87
|
|
52
|
-
def
|
53
|
-
|
88
|
+
def due?
|
89
|
+
scheduled_at.nil? || scheduled_at <= Time.current
|
54
90
|
end
|
55
91
|
|
56
92
|
def discard
|
@@ -66,14 +102,6 @@ module SolidQueue
|
|
66
102
|
end
|
67
103
|
|
68
104
|
private
|
69
|
-
def due?
|
70
|
-
scheduled_at.nil? || scheduled_at <= Time.current
|
71
|
-
end
|
72
|
-
|
73
|
-
def schedule
|
74
|
-
ScheduledExecution.create_or_find_by!(job_id: id)
|
75
|
-
end
|
76
|
-
|
77
105
|
def ready
|
78
106
|
ReadyExecution.create_or_find_by!(job_id: id)
|
79
107
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class Job
|
5
|
+
module Schedulable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
has_one :scheduled_execution, dependent: :destroy
|
10
|
+
|
11
|
+
scope :scheduled, -> { where.not(finished_at: nil) }
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
def schedule_all(jobs)
|
16
|
+
schedule_all_at_once(jobs)
|
17
|
+
successfully_scheduled(jobs)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def schedule_all_at_once(jobs)
|
22
|
+
ScheduledExecution.create_all_from_jobs(jobs)
|
23
|
+
end
|
24
|
+
|
25
|
+
def successfully_scheduled(jobs)
|
26
|
+
where(id: ScheduledExecution.where(job_id: jobs.map(&:id)).pluck(:job_id))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def due?
|
31
|
+
scheduled_at.nil? || scheduled_at <= Time.current
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def schedule
|
36
|
+
ScheduledExecution.create_or_find_by!(job_id: id)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,38 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
module SolidQueue
|
4
|
+
class Job < Record
|
5
|
+
include Executable
|
5
6
|
|
6
|
-
if Gem::Version.new(Rails.version) >= Gem::Version.new("7.1")
|
7
7
|
serialize :arguments, coder: JSON
|
8
|
-
else
|
9
|
-
serialize :arguments, JSON
|
10
|
-
end
|
11
8
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
class << self
|
16
|
-
def enqueue_active_job(active_job, scheduled_at: Time.current)
|
17
|
-
enqueue \
|
18
|
-
queue_name: active_job.queue_name,
|
19
|
-
active_job_id: active_job.job_id,
|
20
|
-
priority: active_job.priority,
|
21
|
-
scheduled_at: scheduled_at,
|
22
|
-
class_name: active_job.class.name,
|
23
|
-
arguments: active_job.serialize,
|
24
|
-
concurrency_key: active_job.try(:concurrency_key)
|
25
|
-
end
|
9
|
+
class << self
|
10
|
+
def enqueue_all(active_jobs)
|
11
|
+
active_jobs_by_job_id = active_jobs.index_by(&:job_id)
|
26
12
|
|
27
|
-
|
28
|
-
|
29
|
-
|
13
|
+
transaction do
|
14
|
+
jobs = create_all_from_active_jobs(active_jobs)
|
15
|
+
prepare_all_for_execution(jobs).tap do |enqueued_jobs|
|
16
|
+
enqueued_jobs.each do |enqueued_job|
|
17
|
+
active_jobs_by_job_id[enqueued_job.active_job_id].provider_job_id = enqueued_job.id
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
30
21
|
end
|
31
|
-
end
|
32
22
|
|
33
|
-
|
34
|
-
|
35
|
-
|
23
|
+
def enqueue(active_job, scheduled_at: Time.current)
|
24
|
+
active_job.scheduled_at = scheduled_at
|
25
|
+
|
26
|
+
create_from_active_job(active_job).tap do |enqueued_job|
|
27
|
+
active_job.provider_job_id = enqueued_job.id
|
28
|
+
end
|
36
29
|
end
|
30
|
+
|
31
|
+
private
|
32
|
+
DEFAULT_PRIORITY = 0
|
33
|
+
DEFAULT_QUEUE_NAME = "default"
|
34
|
+
|
35
|
+
def create_from_active_job(active_job)
|
36
|
+
create!(**attributes_from_active_job(active_job))
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_all_from_active_jobs(active_jobs)
|
40
|
+
job_rows = active_jobs.map { |job| attributes_from_active_job(job) }
|
41
|
+
insert_all(job_rows)
|
42
|
+
where(active_job_id: active_jobs.map(&:job_id))
|
43
|
+
end
|
44
|
+
|
45
|
+
def attributes_from_active_job(active_job)
|
46
|
+
{
|
47
|
+
queue_name: active_job.queue_name || DEFAULT_QUEUE_NAME,
|
48
|
+
active_job_id: active_job.job_id,
|
49
|
+
priority: active_job.priority || DEFAULT_PRIORITY,
|
50
|
+
scheduled_at: active_job.scheduled_at,
|
51
|
+
class_name: active_job.class.name,
|
52
|
+
arguments: active_job.serialize,
|
53
|
+
concurrency_key: active_job.concurrency_key
|
54
|
+
}
|
55
|
+
end
|
56
|
+
end
|
37
57
|
end
|
38
58
|
end
|
@@ -9,7 +9,7 @@ module SolidQueue::Process::Prunable
|
|
9
9
|
|
10
10
|
class_methods do
|
11
11
|
def prune
|
12
|
-
prunable.
|
12
|
+
prunable.non_blocking_lock.find_in_batches(batch_size: 50) do |batch|
|
13
13
|
batch.each do |process|
|
14
14
|
SolidQueue.logger.info("[SolidQueue] Pruning dead process #{process.id} - #{process.metadata}")
|
15
15
|
process.deregister
|
@@ -4,7 +4,7 @@ module SolidQueue
|
|
4
4
|
class ReadyExecution < Execution
|
5
5
|
scope :queued_as, ->(queue_name) { where(queue_name: queue_name) }
|
6
6
|
|
7
|
-
|
7
|
+
assumes_attributes_from_job
|
8
8
|
|
9
9
|
class << self
|
10
10
|
def claim(queue_list, limit, process_id)
|
@@ -26,7 +26,7 @@ module SolidQueue
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def select_candidates(queue_relation, limit)
|
29
|
-
queue_relation.ordered.limit(limit).
|
29
|
+
queue_relation.ordered.limit(limit).non_blocking_lock.pluck(:job_id)
|
30
30
|
end
|
31
31
|
|
32
32
|
def lock_candidates(job_ids, process_id)
|
@@ -6,11 +6,11 @@ module SolidQueue
|
|
6
6
|
|
7
7
|
connects_to **SolidQueue.connects_to if SolidQueue.connects_to
|
8
8
|
|
9
|
-
def self.
|
9
|
+
def self.non_blocking_lock
|
10
10
|
if SolidQueue.use_skip_locked
|
11
|
-
|
11
|
+
lock(Arel.sql("FOR UPDATE SKIP LOCKED"))
|
12
12
|
else
|
13
|
-
|
13
|
+
lock
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -6,12 +6,12 @@ module SolidQueue
|
|
6
6
|
scope :ordered, -> { order(scheduled_at: :asc, priority: :asc) }
|
7
7
|
scope :next_batch, ->(batch_size) { due.ordered.limit(batch_size) }
|
8
8
|
|
9
|
-
|
9
|
+
assumes_attributes_from_job :scheduled_at
|
10
10
|
|
11
11
|
class << self
|
12
12
|
def dispatch_next_batch(batch_size)
|
13
13
|
transaction do
|
14
|
-
job_ids = next_batch(batch_size).
|
14
|
+
job_ids = next_batch(batch_size).non_blocking_lock.pluck(:job_id)
|
15
15
|
if job_ids.empty? then []
|
16
16
|
else
|
17
17
|
dispatch_batch(job_ids)
|
@@ -22,44 +22,12 @@ module SolidQueue
|
|
22
22
|
private
|
23
23
|
def dispatch_batch(job_ids)
|
24
24
|
jobs = Job.where(id: job_ids)
|
25
|
-
with_concurrency_limits, without_concurrency_limits = jobs.partition(&:concurrency_limited?)
|
26
25
|
|
27
|
-
|
28
|
-
dispatch_one_by_one(with_concurrency_limits)
|
29
|
-
|
30
|
-
successfully_dispatched(job_ids).tap do |dispatched_job_ids|
|
26
|
+
Job.dispatch_all(jobs).map(&:id).tap do |dispatched_job_ids|
|
31
27
|
where(job_id: dispatched_job_ids).delete_all
|
32
28
|
SolidQueue.logger.info("[SolidQueue] Dispatched scheduled batch with #{dispatched_job_ids.size} jobs")
|
33
29
|
end
|
34
30
|
end
|
35
|
-
|
36
|
-
def dispatch_at_once(jobs)
|
37
|
-
ReadyExecution.insert_all ready_rows_from_batch(jobs)
|
38
|
-
end
|
39
|
-
|
40
|
-
def dispatch_one_by_one(jobs)
|
41
|
-
jobs.each(&:dispatch)
|
42
|
-
end
|
43
|
-
|
44
|
-
def ready_rows_from_batch(jobs)
|
45
|
-
prepared_at = Time.current
|
46
|
-
|
47
|
-
jobs.map do |job|
|
48
|
-
{ job_id: job.id, queue_name: job.queue_name, priority: job.priority, created_at: prepared_at }
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def successfully_dispatched(job_ids)
|
53
|
-
dispatched_and_ready(job_ids) + dispatched_and_blocked(job_ids)
|
54
|
-
end
|
55
|
-
|
56
|
-
def dispatched_and_ready(job_ids)
|
57
|
-
ReadyExecution.where(job_id: job_ids).pluck(:job_id)
|
58
|
-
end
|
59
|
-
|
60
|
-
def dispatched_and_blocked(job_ids)
|
61
|
-
BlockedExecution.where(job_id: job_ids).pluck(:job_id)
|
62
|
-
end
|
63
31
|
end
|
64
32
|
end
|
65
33
|
end
|
@@ -9,15 +9,15 @@ module ActiveJob
|
|
9
9
|
# Rails.application.config.active_job.queue_adapter = :solid_queue
|
10
10
|
class SolidQueueAdapter
|
11
11
|
def enqueue(active_job) # :nodoc:
|
12
|
-
SolidQueue::Job.
|
13
|
-
active_job.provider_job_id = job.id
|
14
|
-
end
|
12
|
+
SolidQueue::Job.enqueue(active_job)
|
15
13
|
end
|
16
14
|
|
17
15
|
def enqueue_at(active_job, timestamp) # :nodoc:
|
18
|
-
SolidQueue::Job.
|
19
|
-
|
20
|
-
|
16
|
+
SolidQueue::Job.enqueue(active_job, scheduled_at: Time.at(timestamp))
|
17
|
+
end
|
18
|
+
|
19
|
+
def enqueue_all(active_jobs) # :nodoc:
|
20
|
+
SolidQueue::Job.enqueue_all(active_jobs)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class SolidQueue::InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("templates", __dir__)
|
5
|
+
|
4
6
|
class_option :skip_migrations, type: :boolean, default: nil, desc: "Skip migrations"
|
5
7
|
|
6
8
|
def add_solid_queue
|
7
|
-
|
8
|
-
|
9
|
-
gsub_file env_config, /(# )?config\.active_job\.queue_adapter\s+=.*/, "config.active_job.queue_adapter = :solid_queue"
|
10
|
-
end
|
9
|
+
if (env_config = Pathname(destination_root).join("config/environments/production.rb")).exist?
|
10
|
+
gsub_file env_config, /(# )?config\.active_job\.queue_adapter\s+=.*/, "config.active_job.queue_adapter = :solid_queue"
|
11
11
|
end
|
12
|
+
|
13
|
+
copy_file "config.yml", "config/solid_queue.yml"
|
12
14
|
end
|
13
15
|
|
14
16
|
def create_migrations
|
@@ -0,0 +1,18 @@
|
|
1
|
+
#default: &default
|
2
|
+
# dispatchers:
|
3
|
+
# - polling_interval: 1
|
4
|
+
# batch_size: 500
|
5
|
+
# workers:
|
6
|
+
# - queues: "*"
|
7
|
+
# threads: 5
|
8
|
+
# processes: 1
|
9
|
+
# polling_interval: 0.1
|
10
|
+
#
|
11
|
+
# development:
|
12
|
+
# <<: *default
|
13
|
+
#
|
14
|
+
# test:
|
15
|
+
# <<: *default
|
16
|
+
#
|
17
|
+
# production:
|
18
|
+
# <<: *default
|
@@ -6,16 +6,19 @@ Puma::Plugin.create do
|
|
6
6
|
def start(launcher)
|
7
7
|
@log_writer = launcher.log_writer
|
8
8
|
@puma_pid = $$
|
9
|
-
@solid_queue_pid = fork do
|
10
|
-
Thread.new { monitor_puma }
|
11
|
-
SolidQueue::Supervisor.start(mode: :all)
|
12
|
-
end
|
13
9
|
|
14
|
-
launcher.events.
|
10
|
+
launcher.events.on_booted do
|
11
|
+
@solid_queue_pid = fork do
|
12
|
+
Thread.new { monitor_puma }
|
13
|
+
SolidQueue::Supervisor.start(mode: :all)
|
14
|
+
end
|
15
15
|
|
16
|
-
|
17
|
-
|
16
|
+
in_background do
|
17
|
+
monitor_solid_queue
|
18
|
+
end
|
18
19
|
end
|
20
|
+
|
21
|
+
launcher.events.on_stopped { stop_solid_queue }
|
19
22
|
end
|
20
23
|
|
21
24
|
private
|
@@ -73,29 +73,37 @@ module SolidQueue
|
|
73
73
|
.map { |options| options.dup.symbolize_keys }
|
74
74
|
end
|
75
75
|
|
76
|
+
|
76
77
|
def load_config_from(file_or_hash)
|
77
78
|
case file_or_hash
|
78
|
-
when
|
79
|
-
|
80
|
-
when
|
81
|
-
|
82
|
-
|
79
|
+
when Hash
|
80
|
+
file_or_hash.dup
|
81
|
+
when Pathname, String
|
82
|
+
load_config_from_file Pathname.new(file_or_hash)
|
83
|
+
when NilClass
|
84
|
+
load_config_from_env_location || load_config_from_default_location
|
85
|
+
else
|
86
|
+
raise "Solid Queue cannot be initialized with #{file_or_hash.inspect}"
|
83
87
|
end
|
84
88
|
end
|
85
89
|
|
86
|
-
def
|
87
|
-
if
|
88
|
-
|
89
|
-
else
|
90
|
-
raise "Configuration file not found in #{file}"
|
90
|
+
def load_config_from_env_location
|
91
|
+
if ENV["SOLID_QUEUE_CONFIG"].present?
|
92
|
+
load_config_from_file Rails.root.join(ENV["SOLID_QUEUE_CONFIG"])
|
91
93
|
end
|
92
94
|
end
|
93
95
|
|
94
|
-
def
|
95
|
-
|
96
|
+
def load_config_from_default_location
|
97
|
+
Rails.root.join(DEFAULT_CONFIG_FILE_PATH).then do |config_file|
|
98
|
+
config_file.exist? ? load_config_from_file(config_file) : {}
|
99
|
+
end
|
100
|
+
end
|
96
101
|
|
97
|
-
|
98
|
-
|
102
|
+
def load_config_from_file(file)
|
103
|
+
if file.exist?
|
104
|
+
ActiveSupport::ConfigurationFile.parse(file).deep_symbolize_keys
|
105
|
+
else
|
106
|
+
raise "Configuration file for Solid Queue not found in #{file}"
|
99
107
|
end
|
100
108
|
end
|
101
109
|
end
|
@@ -79,7 +79,7 @@ module SolidQueue
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def graceful_termination
|
82
|
-
|
82
|
+
SolidQueue.logger.info("[SolidQueue] Terminating gracefully...")
|
83
83
|
term_forks
|
84
84
|
|
85
85
|
wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
|
@@ -90,7 +90,7 @@ module SolidQueue
|
|
90
90
|
end
|
91
91
|
|
92
92
|
def immediate_termination
|
93
|
-
|
93
|
+
SolidQueue.logger.info("[SolidQueue] Terminating immediately...")
|
94
94
|
quit_forks
|
95
95
|
end
|
96
96
|
|
data/lib/solid_queue/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_queue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.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: 2023-12-
|
11
|
+
date: 2023-12-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 7.
|
19
|
+
version: '7.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 7.
|
26
|
+
version: '7.1'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: debug
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: puma
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
description: Database-backed Active Job backend.
|
56
70
|
email:
|
57
71
|
- rosa@37signals.com
|
@@ -71,6 +85,7 @@ files:
|
|
71
85
|
- app/models/solid_queue/job/clearable.rb
|
72
86
|
- app/models/solid_queue/job/concurrency_controls.rb
|
73
87
|
- app/models/solid_queue/job/executable.rb
|
88
|
+
- app/models/solid_queue/job/schedulable.rb
|
74
89
|
- app/models/solid_queue/pause.rb
|
75
90
|
- app/models/solid_queue/process.rb
|
76
91
|
- app/models/solid_queue/process/prunable.rb
|
@@ -86,6 +101,7 @@ files:
|
|
86
101
|
- lib/active_job/queue_adapters/solid_queue_adapter.rb
|
87
102
|
- lib/generators/solid_queue/install/USAGE
|
88
103
|
- lib/generators/solid_queue/install/install_generator.rb
|
104
|
+
- lib/generators/solid_queue/install/templates/config.yml
|
89
105
|
- lib/puma/plugin/solid_queue.rb
|
90
106
|
- lib/solid_queue.rb
|
91
107
|
- lib/solid_queue/app_executor.rb
|
@@ -127,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
143
|
- !ruby/object:Gem::Version
|
128
144
|
version: '0'
|
129
145
|
requirements: []
|
130
|
-
rubygems_version: 3.4.
|
146
|
+
rubygems_version: 3.4.10
|
131
147
|
signing_key:
|
132
148
|
specification_version: 4
|
133
149
|
summary: Database-backed Active Job backend.
|