solid_queue 0.8.2 → 1.0.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +124 -31
- data/Rakefile +13 -0
- data/UPGRADING.md +10 -8
- data/app/jobs/solid_queue/recurring_job.rb +9 -0
- data/app/models/solid_queue/process/prunable.rb +2 -2
- data/app/models/solid_queue/process.rb +5 -1
- data/app/models/solid_queue/recurring_task.rb +35 -8
- data/lib/active_job/queue_adapters/solid_queue_adapter.rb +1 -1
- data/lib/generators/solid_queue/install/USAGE +1 -0
- data/lib/generators/solid_queue/install/install_generator.rb +2 -1
- data/lib/generators/solid_queue/install/templates/config/recurring.yml +9 -0
- data/lib/solid_queue/cli.rb +11 -2
- data/lib/solid_queue/configuration.rb +67 -42
- data/lib/solid_queue/dispatcher.rb +4 -5
- data/lib/solid_queue/log_subscriber.rb +2 -2
- data/lib/solid_queue/processes/base.rb +9 -0
- data/lib/solid_queue/processes/poller.rb +0 -3
- data/lib/solid_queue/processes/registrable.rb +5 -2
- data/lib/solid_queue/processes/runnable.rb +11 -8
- data/lib/solid_queue/processes/supervised.rb +0 -1
- data/lib/solid_queue/{dispatcher → scheduler}/recurring_schedule.rb +1 -1
- data/lib/solid_queue/scheduler.rb +53 -0
- data/lib/solid_queue/supervisor/maintenance.rb +1 -1
- data/lib/solid_queue/supervisor.rb +3 -8
- data/lib/solid_queue/version.rb +1 -1
- data/lib/solid_queue.rb +0 -2
- metadata +13 -6
- /data/lib/generators/solid_queue/install/templates/config/{solid_queue.yml → queue.yml} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0185a60652945d7afadb3c4c8e646f9445eba35086ab88ddf1b93ff3b271e973'
|
4
|
+
data.tar.gz: 363d19a3e07ced689dd4a3ac3ec36c7cdfeecfc61a5588ccb6f67c28b782a5bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fc522f72cf05273cd6d7fb98cbce44001c28532131952fb1bc139c2c80d808afb2bd7dc8acd337c3fe4834d30e1dc75a908aff7d3984a1e13dfbada8b25261f
|
7
|
+
data.tar.gz: 96c3d62e399468a193dc2f915797308b645eaf8c60b109a4344a757b4d91c4accf80192c4ebcd26022d08f876a8b0e95788250dfe27eb58e0b723082c6147b2d
|
data/README.md
CHANGED
@@ -2,9 +2,9 @@
|
|
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
|
-
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`).
|
5
|
+
Besides regular job enqueuing and processing, Solid Queue supports delayed jobs, concurrency controls, recurring jobs, pausing queues, numeric priorities per job, priorities by queue order, and bulk enqueuing (`enqueue_all` for Active Job's `perform_all_later`).
|
6
6
|
|
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.
|
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's multi-threading.
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
@@ -13,9 +13,9 @@ Solid Queue is configured by default in new Rails 8 applications. But if you're
|
|
13
13
|
1. `bundle add solid_queue`
|
14
14
|
2. `bin/rails solid_queue:install`
|
15
15
|
|
16
|
-
This will configure Solid Queue as the production Active Job backend, create `config/
|
16
|
+
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.
|
17
17
|
|
18
|
-
|
18
|
+
Once you've done that, you will then have to add the configuration for the queue database in `config/database.yml`. If you're using SQLite, it'll look like this:
|
19
19
|
|
20
20
|
```yaml
|
21
21
|
production:
|
@@ -43,12 +43,27 @@ production:
|
|
43
43
|
migrations_paths: db/queue_migrate
|
44
44
|
```
|
45
45
|
|
46
|
+
Note: Calling `bin/rails solid_queue:install` will automatically add `config.solid_queue.connects_to = { database: { writing: :queue } }` to `config/environments/production.rb`, so no additional configuration is needed there (although you must make sure that you use the `queue` name in `database.yml` for this to match!). But if you want to use Solid Queue in a different environment (like staging or even development), you'll have to manually add that `config.solid_queue.connects_to` line to the respective environment file. And, as always, make sure that the name you're using for the database in `config/database.yml` matches the name you use in `config.solid_queue.connects_to`.
|
47
|
+
|
46
48
|
Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
|
47
49
|
|
48
50
|
Now you're ready to start processing jobs by running `bin/jobs` on the server that's doing the work. This will start processing jobs in all queues using the default configuration. See [below](#configuration) to learn more about configuring Solid Queue.
|
49
51
|
|
50
52
|
For small projects, you can run Solid Queue on the same machine as your webserver. When you're ready to scale, Solid Queue supports horizontal scaling out-of-the-box. You can run Solid Queue on a separate server from your webserver, or even run `bin/jobs` on multiple machines at the same time. Depending on the configuration, you can designate some machines to run only dispatchers or only workers. See the [configuration](#configuration) section for more details on this.
|
51
53
|
|
54
|
+
**Note**: future changes to the schema will come in the form of regular migrations.
|
55
|
+
|
56
|
+
|
57
|
+
### Single database configuration
|
58
|
+
|
59
|
+
Running Solid Queue in a separate database is recommended, but it's also possible to use one single database for both the app and the queue. Just follow these steps:
|
60
|
+
|
61
|
+
1. Copy the contents of `db/queue_schema.rb` into a normal migration and delete `db/queue_schema.rb`
|
62
|
+
2. Remove `config.solid_queue.connects_to` from `production.rb`
|
63
|
+
3. Migrate your database. You are ready to run `bin/jobs`
|
64
|
+
|
65
|
+
You won't have multiple databases, so `database.yml` doesn't need to have primary and queue database.
|
66
|
+
|
52
67
|
## Incremental adoption
|
53
68
|
|
54
69
|
If you're planning to adopt Solid Queue incrementally by switching one job at the time, you can do so by leaving the `config.active_job.queue_adapter` set to your old backend, and then set the `queue_adapter` directly in the jobs you're moving:
|
@@ -61,22 +76,31 @@ class MyJob < ApplicationJob
|
|
61
76
|
# ...
|
62
77
|
end
|
63
78
|
```
|
79
|
+
|
64
80
|
## High performance requirements
|
65
81
|
|
66
82
|
Solid Queue was designed for the highest throughput when used 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. You can also use it with SQLite on smaller applications.
|
67
83
|
|
68
84
|
## Configuration
|
69
85
|
|
70
|
-
### Workers and
|
86
|
+
### Workers, dispatchers and scheduler
|
71
87
|
|
72
|
-
We have
|
88
|
+
We have several types of actors in Solid Queue:
|
73
89
|
- _Workers_ are in charge of picking jobs ready to run from queues and processing them. They work off the `solid_queue_ready_executions` table.
|
74
|
-
- _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.
|
90
|
+
- _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. On top of that, they do some maintenance work related to [concurrency controls](#concurrency-controls).
|
91
|
+
- The _scheduler_ manages [recurring tasks](#recurring-tasks), enqueuing jobs for them when they're due.
|
75
92
|
- The _supervisor_ runs workers and dispatchers according to the configuration, controls their heartbeats, and stops and starts them when needed.
|
76
93
|
|
77
|
-
Solid Queue's supervisor will fork a separate process for each supervised worker/dispatcher.
|
94
|
+
Solid Queue's supervisor will fork a separate process for each supervised worker/dispatcher/scheduler.
|
78
95
|
|
79
|
-
By default, Solid Queue will try to find your configuration under `config/
|
96
|
+
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:
|
97
|
+
|
98
|
+
```
|
99
|
+
bin/jobs -c config/calendar.yml
|
100
|
+
```
|
101
|
+
|
102
|
+
|
103
|
+
This is what this configuration looks like:
|
80
104
|
|
81
105
|
```yml
|
82
106
|
production:
|
@@ -105,6 +129,7 @@ production:
|
|
105
129
|
```
|
106
130
|
the supervisor will run 1 dispatcher and no workers.
|
107
131
|
|
132
|
+
|
108
133
|
Here's an overview of the different options:
|
109
134
|
|
110
135
|
- `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.
|
@@ -127,7 +152,7 @@ Here's an overview of the different options:
|
|
127
152
|
- `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.
|
128
153
|
- `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.
|
129
154
|
- `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.
|
130
|
-
|
155
|
+
|
131
156
|
|
132
157
|
### Queue order and priorities
|
133
158
|
|
@@ -198,8 +223,10 @@ There are several settings that control how Solid Queue works that you can set a
|
|
198
223
|
```ruby
|
199
224
|
-> (exception) { Rails.error.report(exception, handled: false) }
|
200
225
|
```
|
226
|
+
|
201
227
|
**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.
|
202
228
|
|
229
|
+
- `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:
|
203
230
|
- `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.
|
204
231
|
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
205
232
|
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
@@ -209,7 +236,6 @@ There are several settings that control how Solid Queue works that you can set a
|
|
209
236
|
- `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`.
|
210
237
|
- `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.
|
211
238
|
- `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.
|
212
|
-
- `enqueue_after_transaction_commit`: whether the job queuing is deferred to after the current Active Record transaction is committed. The default is `false`. [Read more](https://github.com/rails/rails/pull/51426).
|
213
239
|
|
214
240
|
## Errors when enqueuing
|
215
241
|
|
@@ -234,6 +260,9 @@ class MyJob < ApplicationJob
|
|
234
260
|
|
235
261
|
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).
|
236
262
|
|
263
|
+
The concurrency limits use the concept of semaphores when enqueuing, and work as follows: when a job is enqueued, we check if it specifies concurrency controls. If it does, we check the semaphore for the computed concurrency key. If the semaphore is open, we claim it and we set the job as _ready_. Ready means it can be picked up by workers for execution. When the job finishes executing (be it successfully or unsuccessfully, resulting in a failed execution), we signal the semaphore and try to unblock the next job with the same key, if any. Unblocking the next job doesn't mean running that job right away, but moving it from _blocked_ to _ready_. Since something can happen that prevents the first job from releasing the semaphore and unblocking the next job (for example, someone pulling a plug in the machine where the worker is running), we have the `duration` as a failsafe. Jobs that have been blocked for more than duration are candidates to be released, but only as many of them as the concurrency rules allow, as each one would need to go through the semaphore dance check. This means that the `duration` is not really about the job that's enqueued or being run, it's about the jobs that are blocked waiting.
|
264
|
+
|
265
|
+
|
237
266
|
For example:
|
238
267
|
```ruby
|
239
268
|
class DeliverAnnouncementToContactJob < ApplicationJob
|
@@ -242,7 +271,7 @@ class DeliverAnnouncementToContactJob < ApplicationJob
|
|
242
271
|
def perform(contact)
|
243
272
|
# ...
|
244
273
|
```
|
245
|
-
Where `contact` and `account` are `ActiveRecord` records. In this case, we'll ensure that at most two jobs of the kind `DeliverAnnouncementToContact` for the same account will run concurrently. If, for any reason, one of those jobs takes longer than 5 minutes or doesn't release its concurrency lock within 5 minutes of acquiring it, a new job with the same key might gain the lock.
|
274
|
+
Where `contact` and `account` are `ActiveRecord` records. In this case, we'll ensure that at most two jobs of the kind `DeliverAnnouncementToContact` for the same account will run concurrently. If, for any reason, one of those jobs takes longer than 5 minutes or doesn't release its concurrency lock (signals the semaphore) within 5 minutes of acquiring it, a new job with the same key might gain the lock.
|
246
275
|
|
247
276
|
Let's see another example using `group`:
|
248
277
|
|
@@ -268,7 +297,7 @@ Note that the `duration` setting depends indirectly on the value for `concurrenc
|
|
268
297
|
|
269
298
|
Jobs are unblocked in order of priority but queue order is not taken into account for unblocking jobs. That means that if you have a group of jobs that share a concurrency group but are in different queues, or jobs of the same class that you enqueue in different queues, the queue order you set for a worker is not taken into account when unblocking blocked ones. The reason is that a job that runs unblocks the next one, and the job itself doesn't know about a particular worker's queue order (you could even have different workers with different queue orders), it can only know about priority. Once blocked jobs are unblocked and available for polling, they'll be picked up by a worker following its queue order.
|
270
299
|
|
271
|
-
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
|
300
|
+
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 getting an open semaphore, and whenever they get it, they'll be run. It doesn't matter if they had already gotten an open semaphore in the past.
|
272
301
|
|
273
302
|
## Failed jobs and retries
|
274
303
|
|
@@ -283,6 +312,33 @@ failed_execution.discard # This will delete the job from the system
|
|
283
312
|
|
284
313
|
However, we recommend taking a look at [mission_control-jobs](https://github.com/rails/mission_control-jobs), a dashboard where, among other things, you can examine and retry/discard failed jobs.
|
285
314
|
|
315
|
+
### Error reporting on jobs
|
316
|
+
|
317
|
+
Some error tracking services that integrate with Rails, such as Sentry or Rollbar, hook into [Active Job](https://guides.rubyonrails.org/active_job_basics.html#exceptions) and automatically report not handled errors that happen during job execution. However, if your error tracking system doesn't, or if you need some custom reporting, you can hook into Active Job yourself. A possible way of doing this would be:
|
318
|
+
|
319
|
+
```ruby
|
320
|
+
# application_job.rb
|
321
|
+
class ApplicationJob < ActiveJob::Base
|
322
|
+
rescue_from(Exception) do |exception|
|
323
|
+
Rails.error.report(exception)
|
324
|
+
raise exception
|
325
|
+
end
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
Note that, you will have to duplicate the above logic on `ActionMailer::MailDeliveryJob` too. That is because `ActionMailer` doesn't inherit from `ApplicationJob` but instead uses `ActionMailer::MailDeliveryJob` which inherits from `ActiveJob::Base`.
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
# application_mailer.rb
|
333
|
+
|
334
|
+
class ApplicationMailer < ActionMailer::Base
|
335
|
+
ActionMailer::MailDeliveryJob.rescue_from(Exception) do |exception|
|
336
|
+
Rails.error.report(exception)
|
337
|
+
raise exception
|
338
|
+
end
|
339
|
+
end
|
340
|
+
```
|
341
|
+
|
286
342
|
## Puma plugin
|
287
343
|
|
288
344
|
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
|
@@ -291,29 +347,67 @@ plugin :solid_queue
|
|
291
347
|
```
|
292
348
|
to your `puma.rb` configuration.
|
293
349
|
|
350
|
+
|
351
|
+
## Jobs and transactional integrity
|
352
|
+
: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 viceversa, and ensuring that your job won't be enqueued until the transaction within which you're enqueing 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.
|
353
|
+
|
354
|
+
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, **job enqueuing is deferred until any ongoing transaction is committed** thanks to Active Job's built-in capability to do this. This means that even if you run Solid Queue in the same DB as your app, you won't be taking advantage of this transactional integrity.
|
355
|
+
|
356
|
+
If you prefer to change this, you can set [`config.active_job.enqueue_after_transaction_commit`](https://edgeguides.rubyonrails.org/configuring.html#config-active-job-enqueue-after-transaction-commit) to `never`. You can also set this on a per-job basis.
|
357
|
+
|
358
|
+
If you set that to `never` but still want to make sure you're not inadvertently on transactional integrity, you can make sure that:
|
359
|
+
- Your jobs relying on specific data 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.
|
360
|
+
- Or, you configure a different 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:
|
361
|
+
|
362
|
+
```ruby
|
363
|
+
class ApplicationRecord < ActiveRecord::Base
|
364
|
+
self.abstract_class = true
|
365
|
+
|
366
|
+
connects_to database: { writing: :primary, reading: :replica }
|
367
|
+
```
|
368
|
+
|
369
|
+
```ruby
|
370
|
+
config.solid_queue.connects_to = { database: { writing: :primary, reading: :replica } }
|
371
|
+
```
|
372
|
+
|
294
373
|
## Recurring tasks
|
295
374
|
|
296
|
-
Solid Queue supports defining recurring tasks that run at specific times in the future, on a regular basis like cron jobs. These are managed by
|
375
|
+
Solid Queue supports defining recurring tasks that run at specific times in the future, on a regular basis like cron jobs. These are managed by the scheduler process and are defined in their own configuration file. By default, the file is located in `config/recurring.yml`, but you can set a different path using the environment variable `SOLID_QUEUE_RECURRING_SCHEDULE` or by using the `--recurring_schedule_file` option with `bin/jobs`, like this:
|
376
|
+
|
377
|
+
```
|
378
|
+
bin/jobs --recurring_schedule_file=config/schedule.yml
|
379
|
+
```
|
380
|
+
|
381
|
+
The configuration itself looks like this:
|
382
|
+
|
297
383
|
```yml
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
schedule: every second
|
384
|
+
a_periodic_job:
|
385
|
+
class: MyJob
|
386
|
+
args: [ 42, { status: "custom_status" } ]
|
387
|
+
schedule: every second
|
388
|
+
a_cleanup_task:
|
389
|
+
command: "DeletedStuff.clear_all"
|
390
|
+
schedule: every day at 9am
|
306
391
|
```
|
307
|
-
|
392
|
+
|
393
|
+
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.
|
394
|
+
|
395
|
+
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 optionally supply the following for each task:
|
396
|
+
- `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.
|
308
397
|
|
309
398
|
The job in the example configuration above will be enqueued every second as:
|
310
399
|
```ruby
|
311
400
|
MyJob.perform_later(42, status: "custom_status")
|
312
401
|
```
|
313
402
|
|
314
|
-
|
403
|
+
- `queue`: a different queue to be used when enqueuing the job. If none, the queue set up for the job class.
|
404
|
+
|
405
|
+
- `priority`: a numeric priority value to be used when enqueuing the job.
|
406
|
+
|
315
407
|
|
316
|
-
|
408
|
+
Tasks are enqueued at their corresponding times by the scheduler, and each task schedules the next one. This is pretty much [inspired by what GoodJob does](https://github.com/bensheldon/good_job/blob/994ecff5323bf0337e10464841128fda100750e6/lib/good_job/cron_manager.rb).
|
409
|
+
|
410
|
+
It's possible to run multiple schedulers with the same `recurring_tasks` configuration, for example, if you have multiple servers for redundancy, and you run the `scheduler` in more than one of them. To avoid enqueuing duplicate tasks at the same time, an entry in a new `solid_queue_recurring_executions` table is created in the same transaction as the job is enqueued. This table has a unique index on `task_key` and `run_at`, ensuring only one entry per task per time will be created. This only works if you have `preserve_finished_jobs` set to `true` (the default), and the guarantee applies as long as you keep the jobs around.
|
317
411
|
|
318
412
|
Finally, it's possible to configure jobs that aren't handled by Solid Queue. That is, you can have a job like this in your app:
|
319
413
|
```ruby
|
@@ -328,13 +422,12 @@ end
|
|
328
422
|
|
329
423
|
You can still configure this in Solid Queue:
|
330
424
|
```yml
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
args: 22
|
336
|
-
schedule: "*/5 * * * *"
|
425
|
+
my_periodic_resque_job:
|
426
|
+
class: MyResqueJob
|
427
|
+
args: 22
|
428
|
+
schedule: "*/5 * * * *"
|
337
429
|
```
|
430
|
+
|
338
431
|
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.
|
339
432
|
|
340
433
|
## Inspiration
|
data/Rakefile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "bundler/setup"
|
2
4
|
|
3
5
|
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
@@ -6,3 +8,14 @@ load "rails/tasks/engine.rake"
|
|
6
8
|
load "rails/tasks/statistics.rake"
|
7
9
|
|
8
10
|
require "bundler/gem_tasks"
|
11
|
+
|
12
|
+
def databases
|
13
|
+
%w[ mysql postgres sqlite ]
|
14
|
+
end
|
15
|
+
|
16
|
+
task :test do
|
17
|
+
databases.each do |database|
|
18
|
+
sh("TARGET_DB=#{database} bin/setup")
|
19
|
+
sh("TARGET_DB=#{database} bin/rails test")
|
20
|
+
end
|
21
|
+
end
|
data/UPGRADING.md
CHANGED
@@ -1,21 +1,23 @@
|
|
1
|
+
# Upgrading to version 1.x
|
2
|
+
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
|
+
|
4
|
+
# Upgrading to version 0.9.x
|
5
|
+
This version has two breaking changes regarding configuration:
|
6
|
+
- The default configuration file has changed from `config/solid_queue.yml` to `config/queue.yml`.
|
7
|
+
- Recurring tasks are now defined in `config/recurring.yml` (by default). Before, they would be defined as part of the _dispatcher_ configuration. Now they've been upgraded to their own configuration file, and a dedicated process (the _scheduler_) to manage them. Check the _Recurring tasks_ section in the `README` to learn how to configure them in detail. They still follow the same format as before when they lived under `dispatchers > recurring_tasks`.
|
8
|
+
|
1
9
|
# Upgrading to version 0.8.x
|
2
|
-
*IMPORTANT*: This version collapsed all migrations into a single `db/queue_schema.rb`, that will use a separate `queue` database. If you're upgrading from a version < 0.6.0, you need to upgrade to 0.6.0 first, ensure all migrations are up-to-date, and then upgrade further.
|
10
|
+
*IMPORTANT*: This version collapsed all migrations into a single `db/queue_schema.rb`, that will use a separate `queue` database on install. If you're upgrading from a version < 0.6.0, you need to upgrade to 0.6.0 first, ensure all migrations are up-to-date, and then upgrade further. You don't have to switch to a separate `queue` database or use the new `db/queue_schema.rb` file, these are for people starting on a version >= 0.8.x. You can continue using your existing database (be it separate or the same as your app) as long as you run all migrations defined up to version 0.6.0.
|
3
11
|
|
4
12
|
# Upgrading to version 0.7.x
|
5
13
|
|
6
14
|
This version removed the new async mode introduced in version 0.4.0 and introduced a new binstub that can be used to start Solid Queue's supervisor.
|
7
15
|
|
8
|
-
To install
|
16
|
+
To install the binstub `bin/jobs`, you can just run:
|
9
17
|
```
|
10
18
|
bin/rails generate solid_queue:install
|
11
19
|
```
|
12
20
|
|
13
|
-
Or, if you're using a different database for Solid Queue:
|
14
|
-
|
15
|
-
```bash
|
16
|
-
$ bin/rails generate solid_queue:install --database <the_name_of_your_solid_queue_db>
|
17
|
-
```
|
18
|
-
|
19
21
|
|
20
22
|
# Upgrading to version 0.6.x
|
21
23
|
|
@@ -10,9 +10,9 @@ module SolidQueue
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class_methods do
|
13
|
-
def prune
|
13
|
+
def prune(excluding: nil)
|
14
14
|
SolidQueue.instrument :prune_processes, size: 0 do |payload|
|
15
|
-
prunable.non_blocking_lock.find_in_batches(batch_size: 50) do |batch|
|
15
|
+
prunable.excluding(excluding).non_blocking_lock.find_in_batches(batch_size: 50) do |batch|
|
16
16
|
payload[:size] += batch.size
|
17
17
|
|
18
18
|
batch.each(&:prune)
|
@@ -20,7 +20,11 @@ class SolidQueue::Process < SolidQueue::Record
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def heartbeat
|
23
|
-
|
23
|
+
# Clear any previous changes before locking, for example, in case a previous heartbeat
|
24
|
+
# failed because of a DB issue (with SQLite depending on configuration, a BusyException
|
25
|
+
# is not rare) and we still have the unpersisted value
|
26
|
+
restore_attributes
|
27
|
+
with_lock { touch(:last_heartbeat_at) }
|
24
28
|
end
|
25
29
|
|
26
30
|
def deregister(pruned: false)
|
@@ -7,17 +7,30 @@ module SolidQueue
|
|
7
7
|
serialize :arguments, coder: Arguments, default: []
|
8
8
|
|
9
9
|
validate :supported_schedule
|
10
|
+
validate :ensure_command_or_class_present
|
10
11
|
validate :existing_job_class
|
11
12
|
|
12
13
|
scope :static, -> { where(static: true) }
|
13
14
|
|
15
|
+
mattr_accessor :default_job_class
|
16
|
+
self.default_job_class = RecurringJob
|
17
|
+
|
14
18
|
class << self
|
15
19
|
def wrap(args)
|
16
20
|
args.is_a?(self) ? args : from_configuration(args.first, **args.second)
|
17
21
|
end
|
18
22
|
|
19
23
|
def from_configuration(key, **options)
|
20
|
-
new
|
24
|
+
new \
|
25
|
+
key: key,
|
26
|
+
class_name: options[:class],
|
27
|
+
command: options[:command],
|
28
|
+
arguments: options[:args],
|
29
|
+
schedule: options[:schedule],
|
30
|
+
queue_name: options[:queue].presence,
|
31
|
+
priority: options[:priority].presence,
|
32
|
+
description: options[:description],
|
33
|
+
static: true
|
21
34
|
end
|
22
35
|
|
23
36
|
def create_or_update_all(tasks)
|
@@ -47,7 +60,7 @@ module SolidQueue
|
|
47
60
|
else
|
48
61
|
payload[:other_adapter] = true
|
49
62
|
|
50
|
-
perform_later do |job|
|
63
|
+
perform_later.tap do |job|
|
51
64
|
unless job.successfully_enqueued?
|
52
65
|
payload[:enqueue_error] = job.enqueue_error&.message
|
53
66
|
end
|
@@ -77,8 +90,14 @@ module SolidQueue
|
|
77
90
|
end
|
78
91
|
end
|
79
92
|
|
93
|
+
def ensure_command_or_class_present
|
94
|
+
unless command.present? || class_name.present?
|
95
|
+
errors.add :base, :command_and_class_blank, message: "either command or class_name must be present"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
80
99
|
def existing_job_class
|
81
|
-
|
100
|
+
if class_name.present? && job_class.nil?
|
82
101
|
errors.add :class_name, :undefined, message: "doesn't correspond to an existing class"
|
83
102
|
end
|
84
103
|
end
|
@@ -89,7 +108,7 @@ module SolidQueue
|
|
89
108
|
|
90
109
|
def enqueue_and_record(run_at:)
|
91
110
|
RecurringExecution.record(key, run_at) do
|
92
|
-
job_class.new(*arguments_with_kwargs).tap do |active_job|
|
111
|
+
job_class.new(*arguments_with_kwargs).set(enqueue_options).tap do |active_job|
|
93
112
|
active_job.run_callbacks(:enqueue) do
|
94
113
|
Job.enqueue(active_job)
|
95
114
|
end
|
@@ -98,12 +117,16 @@ module SolidQueue
|
|
98
117
|
end
|
99
118
|
end
|
100
119
|
|
101
|
-
def perform_later
|
102
|
-
job_class.
|
120
|
+
def perform_later
|
121
|
+
job_class.new(*arguments_with_kwargs).tap do |active_job|
|
122
|
+
active_job.enqueue(enqueue_options)
|
123
|
+
end
|
103
124
|
end
|
104
125
|
|
105
126
|
def arguments_with_kwargs
|
106
|
-
if
|
127
|
+
if class_name.nil?
|
128
|
+
command
|
129
|
+
elsif arguments.last.is_a?(Hash)
|
107
130
|
arguments[0...-1] + [ Hash.ruby2_keywords_hash(arguments.last) ]
|
108
131
|
else
|
109
132
|
arguments
|
@@ -116,7 +139,11 @@ module SolidQueue
|
|
116
139
|
end
|
117
140
|
|
118
141
|
def job_class
|
119
|
-
@job_class ||= class_name
|
142
|
+
@job_class ||= class_name.present? ? class_name.safe_constantize : self.class.default_job_class
|
143
|
+
end
|
144
|
+
|
145
|
+
def enqueue_options
|
146
|
+
{ queue: queue_name, priority: priority }.compact
|
120
147
|
end
|
121
148
|
end
|
122
149
|
end
|
@@ -4,7 +4,8 @@ class SolidQueue::InstallGenerator < Rails::Generators::Base
|
|
4
4
|
source_root File.expand_path("templates", __dir__)
|
5
5
|
|
6
6
|
def copy_files
|
7
|
-
template "config/
|
7
|
+
template "config/queue.yml"
|
8
|
+
template "config/recurring.yml"
|
8
9
|
template "db/queue_schema.rb"
|
9
10
|
template "bin/jobs"
|
10
11
|
chmod "bin/jobs", 0755 & ~File.umask, verbose: false
|
data/lib/solid_queue/cli.rb
CHANGED
@@ -4,7 +4,16 @@ require "thor"
|
|
4
4
|
|
5
5
|
module SolidQueue
|
6
6
|
class Cli < Thor
|
7
|
-
class_option :config_file, type: :string, aliases: "-c",
|
7
|
+
class_option :config_file, type: :string, aliases: "-c",
|
8
|
+
desc: "Path to config file (default: #{Configuration::DEFAULT_CONFIG_FILE_PATH}).",
|
9
|
+
banner: "SOLID_QUEUE_CONFIG"
|
10
|
+
|
11
|
+
class_option :recurring_schedule_file, type: :string,
|
12
|
+
desc: "Path to recurring schedule definition (default: #{Configuration::DEFAULT_RECURRING_SCHEDULE_FILE_PATH}).",
|
13
|
+
banner: "SOLID_QUEUE_RECURRING_SCHEDULE"
|
14
|
+
|
15
|
+
class_option :skip_recurring, type: :boolean, default: false,
|
16
|
+
desc: "Whether to skip recurring tasks scheduling"
|
8
17
|
|
9
18
|
def self.exit_on_failure?
|
10
19
|
true
|
@@ -14,7 +23,7 @@ module SolidQueue
|
|
14
23
|
default_command :start
|
15
24
|
|
16
25
|
def start
|
17
|
-
SolidQueue::Supervisor.start(
|
26
|
+
SolidQueue::Supervisor.start(**options.symbolize_keys)
|
18
27
|
end
|
19
28
|
end
|
20
29
|
end
|
@@ -19,21 +19,21 @@ module SolidQueue
|
|
19
19
|
batch_size: 500,
|
20
20
|
polling_interval: 1,
|
21
21
|
concurrency_maintenance: true,
|
22
|
-
concurrency_maintenance_interval: 600
|
23
|
-
recurring_tasks: []
|
22
|
+
concurrency_maintenance_interval: 600
|
24
23
|
}
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
dispatchers: [ DISPATCHER_DEFAULTS ]
|
29
|
-
}
|
25
|
+
DEFAULT_CONFIG_FILE_PATH = "config/queue.yml"
|
26
|
+
DEFAULT_RECURRING_SCHEDULE_FILE_PATH = "config/recurring.yml"
|
30
27
|
|
31
|
-
def initialize(
|
32
|
-
@
|
28
|
+
def initialize(**options)
|
29
|
+
@options = options.with_defaults(default_options)
|
33
30
|
end
|
34
31
|
|
35
32
|
def configured_processes
|
36
|
-
|
33
|
+
if only_work? then workers
|
34
|
+
else
|
35
|
+
dispatchers + workers + schedulers
|
36
|
+
end
|
37
37
|
end
|
38
38
|
|
39
39
|
def max_number_of_threads
|
@@ -42,9 +42,29 @@ module SolidQueue
|
|
42
42
|
end
|
43
43
|
|
44
44
|
private
|
45
|
-
attr_reader :
|
45
|
+
attr_reader :options
|
46
|
+
|
47
|
+
def default_options
|
48
|
+
{
|
49
|
+
config_file: Rails.root.join(ENV["SOLID_QUEUE_CONFIG"] || DEFAULT_CONFIG_FILE_PATH),
|
50
|
+
recurring_schedule_file: Rails.root.join(ENV["SOLID_QUEUE_RECURRING_SCHEDULE"] || DEFAULT_RECURRING_SCHEDULE_FILE_PATH),
|
51
|
+
only_work: false,
|
52
|
+
only_dispatch: false,
|
53
|
+
skip_recurring: false
|
54
|
+
}
|
55
|
+
end
|
46
56
|
|
47
|
-
|
57
|
+
def only_work?
|
58
|
+
options[:only_work]
|
59
|
+
end
|
60
|
+
|
61
|
+
def only_dispatch?
|
62
|
+
options[:only_dispatch]
|
63
|
+
end
|
64
|
+
|
65
|
+
def skip_recurring_tasks?
|
66
|
+
options[:skip_recurring] || only_work?
|
67
|
+
end
|
48
68
|
|
49
69
|
def workers
|
50
70
|
workers_options.flat_map do |worker_options|
|
@@ -55,41 +75,58 @@ module SolidQueue
|
|
55
75
|
|
56
76
|
def dispatchers
|
57
77
|
dispatchers_options.map do |dispatcher_options|
|
58
|
-
|
59
|
-
Process.new :dispatcher, dispatcher_options.merge(recurring_tasks: recurring_tasks).with_defaults(DISPATCHER_DEFAULTS)
|
78
|
+
Process.new :dispatcher, dispatcher_options.with_defaults(DISPATCHER_DEFAULTS)
|
60
79
|
end
|
61
80
|
end
|
62
81
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
DEFAULT_CONFIG
|
69
|
-
end
|
82
|
+
def schedulers
|
83
|
+
if !skip_recurring_tasks? && recurring_tasks.any?
|
84
|
+
[ Process.new(:scheduler, recurring_tasks: recurring_tasks) ]
|
85
|
+
else
|
86
|
+
[]
|
70
87
|
end
|
71
88
|
end
|
72
89
|
|
73
90
|
def workers_options
|
74
|
-
@workers_options ||=
|
91
|
+
@workers_options ||= processes_config.fetch(:workers, [])
|
75
92
|
.map { |options| options.dup.symbolize_keys }
|
76
93
|
end
|
77
94
|
|
78
95
|
def dispatchers_options
|
79
|
-
@dispatchers_options ||=
|
96
|
+
@dispatchers_options ||= processes_config.fetch(:dispatchers, [])
|
80
97
|
.map { |options| options.dup.symbolize_keys }
|
81
98
|
end
|
82
99
|
|
83
|
-
def
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
def parse_recurring_tasks(tasks)
|
88
|
-
Array(tasks).map do |id, options|
|
100
|
+
def recurring_tasks
|
101
|
+
@recurring_tasks ||= recurring_tasks_config.map do |id, options|
|
89
102
|
RecurringTask.from_configuration(id, **options)
|
90
103
|
end.select(&:valid?)
|
91
104
|
end
|
92
105
|
|
106
|
+
def processes_config
|
107
|
+
@processes_config ||= config_from \
|
108
|
+
options.slice(:workers, :dispatchers).presence || options[:config_file],
|
109
|
+
keys: [ :workers, :dispatchers ],
|
110
|
+
fallback: { workers: [ WORKER_DEFAULTS ], dispatchers: [ DISPATCHER_DEFAULTS ] }
|
111
|
+
end
|
112
|
+
|
113
|
+
def recurring_tasks_config
|
114
|
+
@recurring_tasks ||= config_from options[:recurring_schedule_file]
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def config_from(file_or_hash, keys: [], fallback: {}, env: Rails.env)
|
119
|
+
load_config_from(file_or_hash).then do |config|
|
120
|
+
config = config[env.to_sym] ? config[env.to_sym] : config
|
121
|
+
config = config.slice(*keys) if keys.any? && config.present?
|
122
|
+
|
123
|
+
if config.empty? then fallback
|
124
|
+
else
|
125
|
+
config
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
93
130
|
def load_config_from(file_or_hash)
|
94
131
|
case file_or_hash
|
95
132
|
when Hash
|
@@ -97,29 +134,17 @@ module SolidQueue
|
|
97
134
|
when Pathname, String
|
98
135
|
load_config_from_file Pathname.new(file_or_hash)
|
99
136
|
when NilClass
|
100
|
-
|
137
|
+
{}
|
101
138
|
else
|
102
139
|
raise "Solid Queue cannot be initialized with #{file_or_hash.inspect}"
|
103
140
|
end
|
104
141
|
end
|
105
142
|
|
106
|
-
def load_config_from_env_location
|
107
|
-
if ENV["SOLID_QUEUE_CONFIG"].present?
|
108
|
-
load_config_from_file Rails.root.join(ENV["SOLID_QUEUE_CONFIG"])
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def load_config_from_default_location
|
113
|
-
Rails.root.join(DEFAULT_CONFIG_FILE_PATH).then do |config_file|
|
114
|
-
config_file.exist? ? load_config_from_file(config_file) : {}
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
143
|
def load_config_from_file(file)
|
119
144
|
if file.exist?
|
120
145
|
ActiveSupport::ConfigurationFile.parse(file).deep_symbolize_keys
|
121
146
|
else
|
122
|
-
|
147
|
+
{}
|
123
148
|
end
|
124
149
|
end
|
125
150
|
end
|
@@ -2,10 +2,10 @@
|
|
2
2
|
|
3
3
|
module SolidQueue
|
4
4
|
class Dispatcher < Processes::Poller
|
5
|
-
attr_accessor :batch_size, :concurrency_maintenance
|
5
|
+
attr_accessor :batch_size, :concurrency_maintenance
|
6
6
|
|
7
|
-
after_boot :start_concurrency_maintenance
|
8
|
-
before_shutdown :stop_concurrency_maintenance
|
7
|
+
after_boot :start_concurrency_maintenance
|
8
|
+
before_shutdown :stop_concurrency_maintenance
|
9
9
|
|
10
10
|
def initialize(**options)
|
11
11
|
options = options.dup.with_defaults(SolidQueue::Configuration::DISPATCHER_DEFAULTS)
|
@@ -13,13 +13,12 @@ module SolidQueue
|
|
13
13
|
@batch_size = options[:batch_size]
|
14
14
|
|
15
15
|
@concurrency_maintenance = ConcurrencyMaintenance.new(options[:concurrency_maintenance_interval], options[:batch_size]) if options[:concurrency_maintenance]
|
16
|
-
@recurring_schedule = RecurringSchedule.new(options[:recurring_tasks])
|
17
16
|
|
18
17
|
super(**options)
|
19
18
|
end
|
20
19
|
|
21
20
|
def metadata
|
22
|
-
super.merge(batch_size: batch_size, concurrency_maintenance_interval: concurrency_maintenance&.interval
|
21
|
+
super.merge(batch_size: batch_size, concurrency_maintenance_interval: concurrency_maintenance&.interval)
|
23
22
|
end
|
24
23
|
|
25
24
|
private
|
@@ -94,7 +94,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
94
94
|
if error = event.payload[:error]
|
95
95
|
warn formatted_event(event, action: "Error registering #{process_kind}", **attributes.merge(error: formatted_error(error)))
|
96
96
|
else
|
97
|
-
|
97
|
+
debug formatted_event(event, action: "Register #{process_kind}", **attributes)
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
@@ -114,7 +114,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
114
114
|
if error = event.payload[:error]
|
115
115
|
warn formatted_event(event, action: "Error deregistering #{process.kind}", **attributes.merge(error: formatted_error(error)))
|
116
116
|
else
|
117
|
-
|
117
|
+
debug formatted_event(event, action: "Deregister #{process.kind}", **attributes)
|
118
118
|
end
|
119
119
|
end
|
120
120
|
|
@@ -10,6 +10,7 @@ module SolidQueue
|
|
10
10
|
|
11
11
|
def initialize(*)
|
12
12
|
@name = generate_name
|
13
|
+
@stopped = false
|
13
14
|
end
|
14
15
|
|
15
16
|
def kind
|
@@ -28,10 +29,18 @@ module SolidQueue
|
|
28
29
|
{}
|
29
30
|
end
|
30
31
|
|
32
|
+
def stop
|
33
|
+
@stopped = true
|
34
|
+
end
|
35
|
+
|
31
36
|
private
|
32
37
|
def generate_name
|
33
38
|
[ kind.downcase, SecureRandom.hex(10) ].join("-")
|
34
39
|
end
|
40
|
+
|
41
|
+
def stopped?
|
42
|
+
@stopped
|
43
|
+
end
|
35
44
|
end
|
36
45
|
end
|
37
46
|
end
|
@@ -29,11 +29,11 @@ module SolidQueue::Processes
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def deregister
|
32
|
-
process
|
32
|
+
process&.deregister
|
33
33
|
end
|
34
34
|
|
35
35
|
def registered?
|
36
|
-
process
|
36
|
+
process.present?
|
37
37
|
end
|
38
38
|
|
39
39
|
def launch_heartbeat
|
@@ -54,6 +54,9 @@ module SolidQueue::Processes
|
|
54
54
|
|
55
55
|
def heartbeat
|
56
56
|
process.heartbeat
|
57
|
+
rescue ActiveRecord::RecordNotFound
|
58
|
+
self.process = nil
|
59
|
+
wake_up
|
57
60
|
end
|
58
61
|
end
|
59
62
|
end
|
@@ -17,7 +17,9 @@ module SolidQueue::Processes
|
|
17
17
|
end
|
18
18
|
|
19
19
|
def stop
|
20
|
-
|
20
|
+
super
|
21
|
+
|
22
|
+
wake_up
|
21
23
|
@thread&.join
|
22
24
|
end
|
23
25
|
|
@@ -31,8 +33,6 @@ module SolidQueue::Processes
|
|
31
33
|
def boot
|
32
34
|
SolidQueue.instrument(:start_process, process: self) do
|
33
35
|
run_callbacks(:boot) do
|
34
|
-
@stopped = false
|
35
|
-
|
36
36
|
if running_as_fork?
|
37
37
|
register_signal_handlers
|
38
38
|
set_procline
|
@@ -41,18 +41,18 @@ module SolidQueue::Processes
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
def run
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
44
48
|
def shutting_down?
|
45
|
-
stopped? || (running_as_fork? && supervisor_went_away?) || finished?
|
49
|
+
stopped? || (running_as_fork? && supervisor_went_away?) || finished? || !registered?
|
46
50
|
end
|
47
51
|
|
48
52
|
def run
|
49
53
|
raise NotImplementedError
|
50
54
|
end
|
51
55
|
|
52
|
-
def stopped?
|
53
|
-
@stopped
|
54
|
-
end
|
55
|
-
|
56
56
|
def finished?
|
57
57
|
running_inline? && all_work_completed?
|
58
58
|
end
|
@@ -61,6 +61,9 @@ module SolidQueue::Processes
|
|
61
61
|
false
|
62
62
|
end
|
63
63
|
|
64
|
+
def shutdown
|
65
|
+
end
|
66
|
+
|
64
67
|
def set_procline
|
65
68
|
end
|
66
69
|
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class Scheduler < Processes::Base
|
5
|
+
include Processes::Runnable
|
6
|
+
|
7
|
+
attr_accessor :recurring_schedule
|
8
|
+
|
9
|
+
after_boot :schedule_recurring_tasks
|
10
|
+
before_shutdown :unschedule_recurring_tasks
|
11
|
+
|
12
|
+
def initialize(recurring_tasks:, **options)
|
13
|
+
@recurring_schedule = RecurringSchedule.new(recurring_tasks)
|
14
|
+
|
15
|
+
super(**options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def metadata
|
19
|
+
super.merge(recurring_schedule: recurring_schedule.task_keys.presence)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
SLEEP_INTERVAL = 60 # Right now it doesn't matter, can be set to 1 in the future for dynamic tasks
|
24
|
+
|
25
|
+
def run
|
26
|
+
loop do
|
27
|
+
break if shutting_down?
|
28
|
+
|
29
|
+
interruptible_sleep(SLEEP_INTERVAL)
|
30
|
+
end
|
31
|
+
ensure
|
32
|
+
SolidQueue.instrument(:shutdown_process, process: self) do
|
33
|
+
run_callbacks(:shutdown) { shutdown }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def schedule_recurring_tasks
|
38
|
+
recurring_schedule.schedule_tasks
|
39
|
+
end
|
40
|
+
|
41
|
+
def unschedule_recurring_tasks
|
42
|
+
recurring_schedule.unschedule_tasks
|
43
|
+
end
|
44
|
+
|
45
|
+
def all_work_completed?
|
46
|
+
recurring_schedule.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_procline
|
50
|
+
procline "scheduling #{recurring_schedule.task_keys.join(",")}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -6,9 +6,9 @@ module SolidQueue
|
|
6
6
|
include Maintenance, Signals, Pidfiled
|
7
7
|
|
8
8
|
class << self
|
9
|
-
def start(
|
9
|
+
def start(**options)
|
10
10
|
SolidQueue.supervisor = true
|
11
|
-
configuration = Configuration.new(
|
11
|
+
configuration = Configuration.new(**options)
|
12
12
|
|
13
13
|
if configuration.configured_processes.any?
|
14
14
|
new(configuration).tap(&:start)
|
@@ -37,7 +37,7 @@ module SolidQueue
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def stop
|
40
|
-
|
40
|
+
super
|
41
41
|
run_stop_hooks
|
42
42
|
end
|
43
43
|
|
@@ -47,7 +47,6 @@ module SolidQueue
|
|
47
47
|
def boot
|
48
48
|
SolidQueue.instrument(:start_process, process: self) do
|
49
49
|
run_callbacks(:boot) do
|
50
|
-
@stopped = false
|
51
50
|
sync_std_streams
|
52
51
|
end
|
53
52
|
end
|
@@ -87,10 +86,6 @@ module SolidQueue
|
|
87
86
|
forks[pid] = process_instance
|
88
87
|
end
|
89
88
|
|
90
|
-
def stopped?
|
91
|
-
@stopped
|
92
|
-
end
|
93
|
-
|
94
89
|
def set_procline
|
95
90
|
procline "supervising #{supervised_processes.join(", ")}"
|
96
91
|
end
|
data/lib/solid_queue/version.rb
CHANGED
data/lib/solid_queue.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: solid_queue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0.beta
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rosa Gutierrez
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-09-
|
11
|
+
date: 2024-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -203,6 +203,7 @@ files:
|
|
203
203
|
- README.md
|
204
204
|
- Rakefile
|
205
205
|
- UPGRADING.md
|
206
|
+
- app/jobs/solid_queue/recurring_job.rb
|
206
207
|
- app/models/solid_queue/blocked_execution.rb
|
207
208
|
- app/models/solid_queue/claimed_execution.rb
|
208
209
|
- app/models/solid_queue/execution.rb
|
@@ -235,7 +236,8 @@ files:
|
|
235
236
|
- lib/generators/solid_queue/install/USAGE
|
236
237
|
- lib/generators/solid_queue/install/install_generator.rb
|
237
238
|
- lib/generators/solid_queue/install/templates/bin/jobs
|
238
|
-
- lib/generators/solid_queue/install/templates/config/
|
239
|
+
- lib/generators/solid_queue/install/templates/config/queue.yml
|
240
|
+
- lib/generators/solid_queue/install/templates/config/recurring.yml
|
239
241
|
- lib/generators/solid_queue/install/templates/db/queue_schema.rb
|
240
242
|
- lib/puma/plugin/solid_queue.rb
|
241
243
|
- lib/solid_queue.rb
|
@@ -244,7 +246,6 @@ files:
|
|
244
246
|
- lib/solid_queue/configuration.rb
|
245
247
|
- lib/solid_queue/dispatcher.rb
|
246
248
|
- lib/solid_queue/dispatcher/concurrency_maintenance.rb
|
247
|
-
- lib/solid_queue/dispatcher/recurring_schedule.rb
|
248
249
|
- lib/solid_queue/engine.rb
|
249
250
|
- lib/solid_queue/lifecycle_hooks.rb
|
250
251
|
- lib/solid_queue/log_subscriber.rb
|
@@ -260,6 +261,8 @@ files:
|
|
260
261
|
- lib/solid_queue/processes/registrable.rb
|
261
262
|
- lib/solid_queue/processes/runnable.rb
|
262
263
|
- lib/solid_queue/processes/supervised.rb
|
264
|
+
- lib/solid_queue/scheduler.rb
|
265
|
+
- lib/solid_queue/scheduler/recurring_schedule.rb
|
263
266
|
- lib/solid_queue/supervisor.rb
|
264
267
|
- lib/solid_queue/supervisor/maintenance.rb
|
265
268
|
- lib/solid_queue/supervisor/pidfile.rb
|
@@ -276,11 +279,15 @@ metadata:
|
|
276
279
|
homepage_uri: https://github.com/rails/solid_queue
|
277
280
|
source_code_uri: https://github.com/rails/solid_queue
|
278
281
|
post_install_message: |
|
279
|
-
Upgrading to Solid Queue 0.
|
280
|
-
|
282
|
+
Upgrading to Solid Queue 0.9.0? There are some breaking changes about how recurring tasks are configured.
|
283
|
+
|
284
|
+
Upgrading to Solid Queue 0.8.0 from < 0.6.0? You need to upgrade to 0.6.0 first.
|
281
285
|
|
282
286
|
Upgrading to Solid Queue 0.4.x, 0.5.x, 0.6.x or 0.7.x? There are some breaking changes about how Solid Queue is started,
|
283
287
|
configuration and new migrations.
|
288
|
+
|
289
|
+
--> Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
|
290
|
+
for upgrade instructions.
|
284
291
|
rdoc_options: []
|
285
292
|
require_paths:
|
286
293
|
- lib
|
File without changes
|