solid_queue 1.0.2 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +148 -35
- data/app/models/solid_queue/job/clearable.rb +2 -1
- data/app/models/solid_queue/queue.rb +13 -0
- data/app/models/solid_queue/recurring_task.rb +14 -0
- data/lib/generators/solid_queue/install/install_generator.rb +5 -3
- data/lib/solid_queue/app_executor.rb +1 -1
- data/lib/solid_queue/configuration.rb +52 -4
- data/lib/solid_queue/dispatcher.rb +2 -1
- data/lib/solid_queue/log_subscriber.rb +2 -1
- data/lib/solid_queue/processes/interruptible.rb +14 -18
- data/lib/solid_queue/processes/poller.rb +4 -4
- data/lib/solid_queue/scheduler/recurring_schedule.rb +1 -0
- data/lib/solid_queue/supervisor.rb +2 -2
- data/lib/solid_queue/version.rb +1 -1
- data/lib/solid_queue/worker.rb +2 -1
- metadata +34 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c36dbf886c5dd03f4dab75e6c28b671fbd24ce011589bbab25ef44f640686c96
|
4
|
+
data.tar.gz: de499841947c2a12fa43e0e6e11cf8e8e0789c73ed7290b16ecc32ccfc133283
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 07b6b3b7c8a24aafe97b0b02acebd8fb06f2171dbdb41d6d5c779b70422c633df10850fd8dd569e0fc06a99d8ce4a5ecef21a17f0675fb547c3d5495f142473b
|
7
|
+
data.tar.gz: 4b8e7fbebd2a03c2fa25d498146d469dda245c9a2a979bb604257e2519d101e6a5e36c85479c7d1b3cafbdb3fbad6d0b8d564061868445a85fa1a361cad0dd70
|
data/README.md
CHANGED
@@ -6,6 +6,31 @@ Besides regular job enqueuing and processing, Solid Queue supports delayed jobs,
|
|
6
6
|
|
7
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
|
+
## Table of contents
|
10
|
+
|
11
|
+
- [Installation](#installation)
|
12
|
+
- [Single database configuration](#single-database-configuration)
|
13
|
+
- [Incremental adoption](#incremental-adoption)
|
14
|
+
- [High performance requirements](#high-performance-requirements)
|
15
|
+
- [Configuration](#configuration)
|
16
|
+
- [Workers, dispatchers and scheduler](#workers-dispatchers-and-scheduler)
|
17
|
+
- [Queue order and priorities](#queue-order-and-priorities)
|
18
|
+
- [Queues specification and performance](#queues-specification-and-performance)
|
19
|
+
- [Threads, processes and signals](#threads-processes-and-signals)
|
20
|
+
- [Database configuration](#database-configuration)
|
21
|
+
- [Other configuration settings](#other-configuration-settings)
|
22
|
+
- [Lifecycle hooks](#lifecycle-hooks)
|
23
|
+
- [Errors when enqueuing](#errors-when-enqueuing)
|
24
|
+
- [Concurrency controls](#concurrency-controls)
|
25
|
+
- [Failed jobs and retries](#failed-jobs-and-retries)
|
26
|
+
- [Error reporting on jobs](#error-reporting-on-jobs)
|
27
|
+
- [Puma plugin](#puma-plugin)
|
28
|
+
- [Jobs and transactional integrity](#jobs-and-transactional-integrity)
|
29
|
+
- [Recurring tasks](#recurring-tasks)
|
30
|
+
- [Inspiration](#inspiration)
|
31
|
+
- [License](#license)
|
32
|
+
|
33
|
+
|
9
34
|
## Installation
|
10
35
|
|
11
36
|
Solid Queue is configured by default in new Rails 8 applications. But if you're running an earlier version, you can add it manually following these steps:
|
@@ -43,8 +68,6 @@ production:
|
|
43
68
|
migrations_paths: db/queue_migrate
|
44
69
|
```
|
45
70
|
|
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
|
-
|
48
71
|
Then run `db:prepare` in production to ensure the database is created and the schema is loaded.
|
49
72
|
|
50
73
|
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.
|
@@ -53,6 +76,72 @@ For small projects, you can run Solid Queue on the same machine as your webserve
|
|
53
76
|
|
54
77
|
**Note**: future changes to the schema will come in the form of regular migrations.
|
55
78
|
|
79
|
+
### Usage in development and other non-production environments
|
80
|
+
|
81
|
+
Calling `bin/rails solid_queue:install` will automatically add `config.solid_queue.connects_to = { database: { writing: :queue } }` to `config/environments/production.rb`. In order to use Solid Queue in other environments (such as development or staging), you'll need to add a similar configuration(s).
|
82
|
+
|
83
|
+
For example, if you're using SQLite in development, update `database.yml` as follows:
|
84
|
+
|
85
|
+
```diff
|
86
|
+
development:
|
87
|
+
primary:
|
88
|
+
<<: *default
|
89
|
+
database: storage/development.sqlite3
|
90
|
+
+ queue:
|
91
|
+
+ <<: *default
|
92
|
+
+ database: storage/development_queue.sqlite3
|
93
|
+
+ migrations_paths: db/queue_migrate
|
94
|
+
```
|
95
|
+
|
96
|
+
Next, add the following to `development.rb`
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
# Use Solid Queue in Development.
|
100
|
+
config.active_job.queue_adapter = :solid_queue
|
101
|
+
config.solid_queue.connects_to = { database: { writing: :queue } }
|
102
|
+
```
|
103
|
+
|
104
|
+
Once you've added this, run `db:prepare` to create the Solid Queue database and load the schema.
|
105
|
+
|
106
|
+
Finally, in order for jobs to be processed, you'll need to have Solid Queue running. In Development, this can be done via [the Puma plugin](#puma-plugin) as well. In `puma.rb` update the following line:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
# You can either set the env var, or check for development
|
110
|
+
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] || Rails.env.development?
|
111
|
+
```
|
112
|
+
|
113
|
+
You can also just use `bin/jobs`, but in this case you might want to [set a different logger for Solid Queue](#other-configuration-settings) because the default logger will log to `log/development.log` and you won't see anything when you run `bin/jobs`. For example:
|
114
|
+
```ruby
|
115
|
+
config.solid_queue.logger = ActiveSupport::Logger.new(STDOUT)
|
116
|
+
```
|
117
|
+
|
118
|
+
**Note about Action Cable**: If you use Action Cable (or anything dependent on Action Cable, such as Turbo Streams), you will also need to update it to use a database.
|
119
|
+
|
120
|
+
In `config/cable.yml`
|
121
|
+
|
122
|
+
```diff
|
123
|
+
development:
|
124
|
+
- adapter: async
|
125
|
+
+ adapter: solid_cable
|
126
|
+
+ connects_to:
|
127
|
+
+ database:
|
128
|
+
+ writing: cable
|
129
|
+
+ polling_interval: 0.1.seconds
|
130
|
+
+ message_retention: 1.day
|
131
|
+
```
|
132
|
+
|
133
|
+
In `config/database.yml`
|
134
|
+
|
135
|
+
```diff
|
136
|
+
development:
|
137
|
+
primary:
|
138
|
+
<<: *default
|
139
|
+
database: storage/development.sqlite3
|
140
|
+
+ cable:
|
141
|
+
+ <<: *default
|
142
|
+
+ database: storage/development_cable.sqlite3
|
143
|
+
+ migrations_paths: db/cable_migrate
|
144
|
+
```
|
56
145
|
|
57
146
|
### Single database configuration
|
58
147
|
|
@@ -64,7 +153,7 @@ Running Solid Queue in a separate database is recommended, but it's also possibl
|
|
64
153
|
|
65
154
|
You won't have multiple databases, so `database.yml` doesn't need to have primary and queue database.
|
66
155
|
|
67
|
-
|
156
|
+
### Incremental adoption
|
68
157
|
|
69
158
|
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:
|
70
159
|
|
@@ -77,7 +166,7 @@ class MyJob < ApplicationJob
|
|
77
166
|
end
|
78
167
|
```
|
79
168
|
|
80
|
-
|
169
|
+
### High performance requirements
|
81
170
|
|
82
171
|
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.
|
83
172
|
|
@@ -86,6 +175,7 @@ Solid Queue was designed for the highest throughput when used with MySQL 8+ or P
|
|
86
175
|
### Workers, dispatchers and scheduler
|
87
176
|
|
88
177
|
We have several types of actors in Solid Queue:
|
178
|
+
|
89
179
|
- _Workers_ are in charge of picking jobs ready to run from queues and processing them. They work off the `solid_queue_ready_executions` table.
|
90
180
|
- _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
181
|
- The _scheduler_ manages [recurring tasks](#recurring-tasks), enqueuing jobs for them when they're due.
|
@@ -99,7 +189,6 @@ By default, Solid Queue will try to find your configuration under `config/queue.
|
|
99
189
|
bin/jobs -c config/calendar.yml
|
100
190
|
```
|
101
191
|
|
102
|
-
|
103
192
|
This is what this configuration looks like:
|
104
193
|
|
105
194
|
```yml
|
@@ -153,6 +242,7 @@ Here's an overview of the different options:
|
|
153
242
|
Check the sections below on [how queue order behaves combined with priorities](#queue-order-and-priorities), and [how the way you specify the queues per worker might affect performance](#queues-specification-and-performance).
|
154
243
|
|
155
244
|
- `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.
|
245
|
+
It is recommended to set this value less than or equal to the queue database's connection pool size minus 2, as each worker thread uses one connection, and two additional connections are reserved for polling and heartbeat.
|
156
246
|
- `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.
|
157
247
|
- `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.
|
158
248
|
|
@@ -239,7 +329,9 @@ The supervisor is in charge of managing these processes, and it responds to the
|
|
239
329
|
|
240
330
|
When receiving a `QUIT` signal, if workers still have jobs in-flight, these will be returned to the queue when the processes are deregistered.
|
241
331
|
|
242
|
-
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
|
332
|
+
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.
|
333
|
+
|
334
|
+
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::Process::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.
|
243
335
|
|
244
336
|
|
245
337
|
### Database configuration
|
@@ -248,6 +340,32 @@ You can configure the database used by Solid Queue via the `config.solid_queue.c
|
|
248
340
|
|
249
341
|
All the options available to Active Record for multiple databases can be used here.
|
250
342
|
|
343
|
+
### Other configuration settings
|
344
|
+
|
345
|
+
_Note_: The settings in this section should be set in your `config/application.rb` or your environment config like this: `config.solid_queue.silence_polling = true`
|
346
|
+
|
347
|
+
There are several settings that control how Solid Queue works that you can set as well:
|
348
|
+
- `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
|
349
|
+
- `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
|
350
|
+
- `on_thread_error`: custom lambda/Proc to call when there's an error within a Solid Queue thread that takes the exception raised as argument. Defaults to
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
-> (exception) { Rails.error.report(exception, handled: false) }
|
354
|
+
```
|
355
|
+
|
356
|
+
**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.
|
357
|
+
|
358
|
+
- `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.
|
359
|
+
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
360
|
+
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
361
|
+
- `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.
|
362
|
+
- `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `true`.
|
363
|
+
- `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.
|
364
|
+
- `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`.
|
365
|
+
- `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`, which can be configured as [a recurring task](#recurring-tasks).
|
366
|
+
- `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.
|
367
|
+
|
368
|
+
|
251
369
|
## Lifecycle hooks
|
252
370
|
|
253
371
|
In Solid queue, you can hook into two different points in the supervisor's life:
|
@@ -275,30 +393,6 @@ SolidQueue.on_stop { stop_metrics_server }
|
|
275
393
|
|
276
394
|
These can be called several times to add multiple hooks, but it needs to happen before Solid Queue is started. An initializer would be a good place to do this.
|
277
395
|
|
278
|
-
### Other configuration settings
|
279
|
-
|
280
|
-
_Note_: The settings in this section should be set in your `config/application.rb` or your environment config like this: `config.solid_queue.silence_polling = true`
|
281
|
-
|
282
|
-
There are several settings that control how Solid Queue works that you can set as well:
|
283
|
-
- `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
|
284
|
-
- `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
|
285
|
-
- `on_thread_error`: custom lambda/Proc to call when there's an error within a Solid Queue thread that takes the exception raised as argument. Defaults to
|
286
|
-
|
287
|
-
```ruby
|
288
|
-
-> (exception) { Rails.error.report(exception, handled: false) }
|
289
|
-
```
|
290
|
-
|
291
|
-
**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.
|
292
|
-
|
293
|
-
- `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.
|
294
|
-
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
295
|
-
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
296
|
-
- `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.
|
297
|
-
- `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `true`.
|
298
|
-
- `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.
|
299
|
-
- `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`.
|
300
|
-
- `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.
|
301
|
-
- `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.
|
302
396
|
|
303
397
|
## Errors when enqueuing
|
304
398
|
|
@@ -410,15 +504,27 @@ plugin :solid_queue
|
|
410
504
|
```
|
411
505
|
to your `puma.rb` configuration.
|
412
506
|
|
507
|
+
If you're using Puma in development but you don't want to use Solid Queue in development, make sure you avoid the plugin being used, for example using an environment variable like this:
|
508
|
+
```ruby
|
509
|
+
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
|
510
|
+
```
|
511
|
+
that you set in production only. This is what Rails 8's default Puma config looks like. Otherwise, if you're using Puma in development but not Solid Queue, starting Puma would start also Solid Queue supervisor and it'll most likely fail because it won't be properly configured.
|
512
|
+
|
413
513
|
|
414
514
|
## Jobs and transactional integrity
|
415
|
-
: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
|
515
|
+
:warning: Having your jobs in the same ACID-compliant database as your application data enables a powerful yet sharp tool: taking advantage of transactional integrity to ensure some action in your app is not committed unless your job is also committed and vice versa, and ensuring that your job won't be enqueued until the transaction within which you're enqueuing it is committed. This can be very powerful and useful, but it can also backfire if you base some of your logic on this behaviour, and in the future, you move to another active job backend, or if you simply move Solid Queue to its own database, and suddenly the behaviour changes under you. Because this can be quite tricky and many people shouldn't need to worry about it, by default Solid Queue is configured in a different database as the main app.
|
416
516
|
|
417
|
-
|
517
|
+
Starting from Rails 8, an option which doesn't rely on this transactional integrity and which Active Job provides is to defer the enqueueing of a job inside an Active Record transaction until that transaction successfully commits. This option can be set via the [`enqueue_after_transaction_commit`](https://edgeapi.rubyonrails.org/classes/ActiveJob/Enqueuing.html#method-c-enqueue_after_transaction_commit) class method on the job level and is by default disabled. Either it can be enabled for individual jobs or for all jobs through `ApplicationJob`:
|
418
518
|
|
419
|
-
|
519
|
+
```ruby
|
520
|
+
class ApplicationJob < ActiveJob::Base
|
521
|
+
self.enqueue_after_transaction_commit = true
|
522
|
+
end
|
523
|
+
```
|
524
|
+
|
525
|
+
Using this option, you can also use Solid Queue in the same database as your app but not rely on transactional integrity.
|
420
526
|
|
421
|
-
If you set
|
527
|
+
If you don't set this option but still want to make sure you're not inadvertently on transactional integrity, you can make sure that:
|
422
528
|
- 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.
|
423
529
|
- 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:
|
424
530
|
|
@@ -433,6 +539,7 @@ If you set that to `never` but still want to make sure you're not inadvertently
|
|
433
539
|
config.solid_queue.connects_to = { database: { writing: :primary, reading: :replica } }
|
434
540
|
```
|
435
541
|
|
542
|
+
|
436
543
|
## Recurring tasks
|
437
544
|
|
438
545
|
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:
|
@@ -468,9 +575,15 @@ MyJob.perform_later(42, status: "custom_status")
|
|
468
575
|
|
469
576
|
- `priority`: a numeric priority value to be used when enqueuing the job.
|
470
577
|
|
471
|
-
|
472
578
|
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).
|
473
579
|
|
580
|
+
For recurring tasks defined as a `command`, you can also change the job class that runs them as follows:
|
581
|
+
```ruby
|
582
|
+
Rails.application.config.after_initialize do # or to_prepare
|
583
|
+
SolidQueue::RecurringTask.default_job_class = MyRecurringCommandJob
|
584
|
+
end
|
585
|
+
```
|
586
|
+
|
474
587
|
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.
|
475
588
|
|
476
589
|
**Note**: a single recurring schedule is supported, so you can have multiple schedulers using the same schedule, but not multiple schedulers using different configurations.
|
@@ -10,9 +10,10 @@ module SolidQueue
|
|
10
10
|
end
|
11
11
|
|
12
12
|
class_methods do
|
13
|
-
def clear_finished_in_batches(batch_size: 500, finished_before: SolidQueue.clear_finished_jobs_after.ago, class_name: nil)
|
13
|
+
def clear_finished_in_batches(batch_size: 500, finished_before: SolidQueue.clear_finished_jobs_after.ago, class_name: nil, sleep_between_batches: 0)
|
14
14
|
loop do
|
15
15
|
records_deleted = clearable(finished_before: finished_before, class_name: class_name).limit(batch_size).delete_all
|
16
|
+
sleep(sleep_between_batches) if sleep_between_batches > 0
|
16
17
|
break if records_deleted == 0
|
17
18
|
end
|
18
19
|
end
|
@@ -40,6 +40,19 @@ module SolidQueue
|
|
40
40
|
@size ||= ReadyExecution.queued_as(name).count
|
41
41
|
end
|
42
42
|
|
43
|
+
def latency
|
44
|
+
@latency ||= begin
|
45
|
+
now = Time.current
|
46
|
+
oldest_enqueued_at = ReadyExecution.queued_as(name).minimum(:created_at) || now
|
47
|
+
|
48
|
+
(now - oldest_enqueued_at).to_i
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def human_latency
|
53
|
+
ActiveSupport::Duration.build(latency).inspect
|
54
|
+
end
|
55
|
+
|
43
56
|
def ==(queue)
|
44
57
|
name == queue.name
|
45
58
|
end
|
@@ -12,6 +12,8 @@ module SolidQueue
|
|
12
12
|
|
13
13
|
scope :static, -> { where(static: true) }
|
14
14
|
|
15
|
+
has_many :recurring_executions, foreign_key: :task_key, primary_key: :key
|
16
|
+
|
15
17
|
mattr_accessor :default_job_class
|
16
18
|
self.default_job_class = RecurringJob
|
17
19
|
|
@@ -53,6 +55,18 @@ module SolidQueue
|
|
53
55
|
parsed_schedule.next_time.utc
|
54
56
|
end
|
55
57
|
|
58
|
+
def previous_time
|
59
|
+
parsed_schedule.previous_time.utc
|
60
|
+
end
|
61
|
+
|
62
|
+
def last_enqueued_time
|
63
|
+
if recurring_executions.loaded?
|
64
|
+
recurring_executions.map(&:run_at).max
|
65
|
+
else
|
66
|
+
recurring_executions.maximum(:run_at)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
56
70
|
def enqueue(at:)
|
57
71
|
SolidQueue.instrument(:enqueue_recurring_task, task: key, at: at) do |payload|
|
58
72
|
active_job = if using_solid_queue_adapter?
|
@@ -11,9 +11,11 @@ class SolidQueue::InstallGenerator < Rails::Generators::Base
|
|
11
11
|
chmod "bin/jobs", 0755 & ~File.umask, verbose: false
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
14
|
+
def configure_adapter_and_database
|
15
|
+
pathname = Pathname(destination_root).join("config/environments/production.rb")
|
16
|
+
|
17
|
+
gsub_file pathname, /\n\s*config\.solid_queue\.connects_to\s+=.*\n/, "\n", verbose: false
|
18
|
+
gsub_file pathname, /(# )?config\.active_job\.queue_adapter\s+=.*\n/,
|
17
19
|
"config.active_job.queue_adapter = :solid_queue\n" +
|
18
20
|
" config.solid_queue.connects_to = { database: { writing: :queue } }\n"
|
19
21
|
end
|
@@ -2,6 +2,12 @@
|
|
2
2
|
|
3
3
|
module SolidQueue
|
4
4
|
class Configuration
|
5
|
+
include ActiveModel::Model
|
6
|
+
|
7
|
+
validate :ensure_configured_processes
|
8
|
+
validate :ensure_valid_recurring_tasks
|
9
|
+
validate :ensure_correctly_sized_thread_pool
|
10
|
+
|
5
11
|
class Process < Struct.new(:kind, :attributes)
|
6
12
|
def instantiate
|
7
13
|
"SolidQueue::#{kind.to_s.titleize}".safe_constantize.new(**attributes)
|
@@ -36,14 +42,46 @@ module SolidQueue
|
|
36
42
|
end
|
37
43
|
end
|
38
44
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
45
|
+
def error_messages
|
46
|
+
if configured_processes.none?
|
47
|
+
"No workers or processed configured. Exiting..."
|
48
|
+
else
|
49
|
+
error_messages = invalid_tasks.map do |task|
|
50
|
+
all_messages = task.errors.full_messages.map { |msg| "\t#{msg}" }.join("\n")
|
51
|
+
"#{task.key}:\n#{all_messages}"
|
52
|
+
end
|
53
|
+
.join("\n")
|
54
|
+
|
55
|
+
"Invalid processes configured:\n#{error_messages}"
|
56
|
+
end
|
42
57
|
end
|
43
58
|
|
44
59
|
private
|
45
60
|
attr_reader :options
|
46
61
|
|
62
|
+
def ensure_configured_processes
|
63
|
+
unless configured_processes.any?
|
64
|
+
errors.add(:base, "No processes configured")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def ensure_valid_recurring_tasks
|
69
|
+
unless skip_recurring_tasks? || invalid_tasks.none?
|
70
|
+
error_messages = invalid_tasks.map do |task|
|
71
|
+
"- #{task.key}: #{task.errors.full_messages.join(", ")}"
|
72
|
+
end
|
73
|
+
|
74
|
+
errors.add(:base, "Invalid recurring tasks:\n#{error_messages.join("\n")}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def ensure_correctly_sized_thread_pool
|
79
|
+
if (db_pool_size = SolidQueue::Record.connection_pool&.size) && db_pool_size < estimated_number_of_threads
|
80
|
+
errors.add(:base, "Solid Queue is configured to use #{estimated_number_of_threads} threads but the " +
|
81
|
+
"database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
47
85
|
def default_options
|
48
86
|
{
|
49
87
|
config_file: Rails.root.join(ENV["SOLID_QUEUE_CONFIG"] || DEFAULT_CONFIG_FILE_PATH),
|
@@ -54,6 +92,10 @@ module SolidQueue
|
|
54
92
|
}
|
55
93
|
end
|
56
94
|
|
95
|
+
def invalid_tasks
|
96
|
+
recurring_tasks.select(&:invalid?)
|
97
|
+
end
|
98
|
+
|
57
99
|
def only_work?
|
58
100
|
options[:only_work]
|
59
101
|
end
|
@@ -100,7 +142,7 @@ module SolidQueue
|
|
100
142
|
def recurring_tasks
|
101
143
|
@recurring_tasks ||= recurring_tasks_config.map do |id, options|
|
102
144
|
RecurringTask.from_configuration(id, **options)
|
103
|
-
end
|
145
|
+
end
|
104
146
|
end
|
105
147
|
|
106
148
|
def processes_config
|
@@ -147,5 +189,11 @@ module SolidQueue
|
|
147
189
|
{}
|
148
190
|
end
|
149
191
|
end
|
192
|
+
|
193
|
+
def estimated_number_of_threads
|
194
|
+
# At most "threads" in each worker + 1 thread for the worker + 1 thread for the heartbeat task
|
195
|
+
thread_count = workers_options.map { |options| options.fetch(:threads, WORKER_DEFAULTS[:threads]) }.max
|
196
|
+
(thread_count || 1) + 2
|
197
|
+
end
|
150
198
|
end
|
151
199
|
end
|
@@ -145,6 +145,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
145
145
|
end
|
146
146
|
|
147
147
|
def replace_fork(event)
|
148
|
+
supervisor_pid = event.payload[:supervisor_pid]
|
148
149
|
status = event.payload[:status]
|
149
150
|
attributes = event.payload.slice(:pid).merge \
|
150
151
|
status: (status.exitstatus || "no exit status set"),
|
@@ -155,7 +156,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
155
156
|
|
156
157
|
if replaced_fork = event.payload[:fork]
|
157
158
|
info formatted_event(event, action: "Replaced terminated #{replaced_fork.kind}", **attributes.merge(hostname: replaced_fork.hostname, name: replaced_fork.name))
|
158
|
-
|
159
|
+
elsif supervisor_pid != 1 # Running Docker, possibly having some processes that have been reparented
|
159
160
|
warn formatted_event(event, action: "Tried to replace forked process but it had already died", **attributes)
|
160
161
|
end
|
161
162
|
end
|
@@ -7,31 +7,27 @@ module SolidQueue::Processes
|
|
7
7
|
end
|
8
8
|
|
9
9
|
private
|
10
|
-
SELF_PIPE_BLOCK_SIZE = 11
|
11
10
|
|
12
11
|
def interrupt
|
13
|
-
|
14
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
15
|
-
# Ignore writes that would block and retry
|
16
|
-
# if another signal arrived while writing
|
17
|
-
retry
|
12
|
+
queue << true
|
18
13
|
end
|
19
14
|
|
15
|
+
# Sleeps for 'time'. Can be interrupted asynchronously and return early via wake_up.
|
16
|
+
# @param time [Numeric] the time to sleep. 0 returns immediately.
|
17
|
+
# @return [true, nil]
|
18
|
+
# * returns `true` if an interrupt was requested via #wake_up between the
|
19
|
+
# last call to `interruptible_sleep` and now, resulting in an early return.
|
20
|
+
# * returns `nil` if it slept the full `time` and was not interrupted.
|
20
21
|
def interruptible_sleep(time)
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
# Invoking this from the main thread may result in significant slowdown.
|
23
|
+
# Utilizing asynchronous execution (Futures) addresses this performance issue.
|
24
|
+
Concurrent::Promises.future(time) do |timeout|
|
25
|
+
queue.pop(timeout:).tap { queue.clear }
|
26
|
+
end.value
|
25
27
|
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
@self_pipe ||= create_self_pipe
|
30
|
-
end
|
31
|
-
|
32
|
-
def create_self_pipe
|
33
|
-
reader, writer = IO.pipe
|
34
|
-
{ reader: reader, writer: writer }
|
29
|
+
def queue
|
30
|
+
@queue ||= Queue.new
|
35
31
|
end
|
36
32
|
end
|
37
33
|
end
|
@@ -25,11 +25,11 @@ module SolidQueue::Processes
|
|
25
25
|
loop do
|
26
26
|
break if shutting_down?
|
27
27
|
|
28
|
-
wrap_in_app_executor do
|
29
|
-
|
30
|
-
interruptible_sleep(polling_interval)
|
31
|
-
end
|
28
|
+
delay = wrap_in_app_executor do
|
29
|
+
poll
|
32
30
|
end
|
31
|
+
|
32
|
+
interruptible_sleep(delay)
|
33
33
|
end
|
34
34
|
ensure
|
35
35
|
SolidQueue.instrument(:shutdown_process, process: self) do
|
@@ -10,10 +10,10 @@ module SolidQueue
|
|
10
10
|
SolidQueue.supervisor = true
|
11
11
|
configuration = Configuration.new(**options)
|
12
12
|
|
13
|
-
if configuration.
|
13
|
+
if configuration.valid?
|
14
14
|
new(configuration).tap(&:start)
|
15
15
|
else
|
16
|
-
abort "
|
16
|
+
abort configuration.errors.full_messages.join("\n") + "\nExiting..."
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
data/lib/solid_queue/version.rb
CHANGED
data/lib/solid_queue/worker.rb
CHANGED
@@ -7,6 +7,7 @@ module SolidQueue
|
|
7
7
|
after_boot :run_start_hooks
|
8
8
|
before_shutdown :run_stop_hooks
|
9
9
|
|
10
|
+
|
10
11
|
attr_accessor :queues, :pool
|
11
12
|
|
12
13
|
def initialize(**options)
|
@@ -29,7 +30,7 @@ module SolidQueue
|
|
29
30
|
pool.post(execution)
|
30
31
|
end
|
31
32
|
|
32
|
-
|
33
|
+
pool.idle? ? polling_interval : 10.minutes
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
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.1.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: 2024-
|
11
|
+
date: 2024-12-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -98,16 +98,16 @@ dependencies:
|
|
98
98
|
name: debug
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
|
-
- - "
|
101
|
+
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
103
|
+
version: '1.9'
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
|
-
- - "
|
108
|
+
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '
|
110
|
+
version: '1.9'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: mocha
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -192,6 +192,34 @@ dependencies:
|
|
192
192
|
- - ">="
|
193
193
|
- !ruby/object:Gem::Version
|
194
194
|
version: '0'
|
195
|
+
- !ruby/object:Gem::Dependency
|
196
|
+
name: rdoc
|
197
|
+
requirement: !ruby/object:Gem::Requirement
|
198
|
+
requirements:
|
199
|
+
- - ">="
|
200
|
+
- !ruby/object:Gem::Version
|
201
|
+
version: '0'
|
202
|
+
type: :development
|
203
|
+
prerelease: false
|
204
|
+
version_requirements: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: logger
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
195
223
|
description: Database-backed Active Job backend.
|
196
224
|
email:
|
197
225
|
- rosa@37signals.com
|