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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b975d1366456d8f53686e0cee4f14e35dfc5461b206de7553818a109f6ae0088
4
- data.tar.gz: 3d1084ecd73a3cd5c557d89b9edf36bac7db5c1f9ce2e995811c31370c261b6b
3
+ metadata.gz: '0185a60652945d7afadb3c4c8e646f9445eba35086ab88ddf1b93ff3b271e973'
4
+ data.tar.gz: 363d19a3e07ced689dd4a3ac3ec36c7cdfeecfc61a5588ccb6f67c28b782a5bf
5
5
  SHA512:
6
- metadata.gz: e06a86af9d282440726a2dfd81d03637d093e7a12fca7f43be3a1f28312d9117c90ce778a7c04923eb5a57feb2909721d1da9c32f0d5b640dc3c09cf3a3c98db
7
- data.tar.gz: 888f76de27a21e99cc64835b005a140b555b812a06f32b7c7588c5b423e8f84301760c653bcfa2a4020def4d6aa3f8c5464684277bf26833355e4cc82cf684d5
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/solid_queue.yml`, and create the `db/queue_schema.rb`.
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
- 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:
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 dispatchers
86
+ ### Workers, dispatchers and scheduler
71
87
 
72
- We have three types of actors in Solid Queue:
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. They're also in charge of managing [recurring tasks](#recurring-tasks), dispatching jobs to process them according to their schedule. On top of that, they do some maintenance work related to [concurrency controls](#concurrency-controls).
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/solid_queue.yml`, but you can set a different path using the environment variable `SOLID_QUEUE_CONFIG`. This is what this configuration looks like:
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
- - `recurring_tasks`: a list of recurring tasks the dispatcher will manage. Read more details about this one in the [Recurring tasks](#recurring-tasks) section.
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 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.
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 dispatcher processes and as such, they can be defined in the dispatcher's configuration like this:
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
- dispatchers:
299
- - polling_interval: 1
300
- batch_size: 500
301
- recurring_tasks:
302
- my_periodic_job:
303
- class: MyJob
304
- args: [ 42, { status: "custom_status" } ]
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
- `recurring_tasks` is a hash/dictionary, and the key will be the task key internally. Each task needs to have a class, which will be the job class to enqueue, and a schedule. The schedule 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 also provide 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.
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
- Tasks are enqueued at their corresponding times by the dispatcher that owns them, 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).
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
- It's possible to run multiple dispatchers with the same `recurring_tasks` configuration. 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.
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
- dispatchers:
332
- - recurring_tasks:
333
- my_periodic_resque_job:
334
- class: MyResqueJob
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 both the binstub `bin/jobs` and the migration, you can just run
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
 
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidQueue::RecurringJob < ActiveJob::Base
4
+ queue_as :solid_queue_recurring
5
+
6
+ def perform(command)
7
+ eval(command)
8
+ end
9
+ end
@@ -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
- touch(:last_heartbeat_at)
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(key: key, class_name: options[:class], schedule: options[:schedule], arguments: options[:args])
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
- unless job_class.present?
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(&block)
102
- job_class.perform_later(*arguments_with_kwargs, &block)
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 arguments.last.is_a?(Hash)
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&.safe_constantize
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
@@ -9,7 +9,7 @@ module ActiveJob
9
9
  # Rails.application.config.active_job.queue_adapter = :solid_queue
10
10
  class SolidQueueAdapter
11
11
  def enqueue_after_transaction_commit?
12
- SolidQueue.enqueue_after_transaction_commit
12
+ true
13
13
  end
14
14
 
15
15
  def enqueue(active_job) # :nodoc:
@@ -6,5 +6,6 @@ Example:
6
6
 
7
7
  This will perform the following:
8
8
  Adds solid_queue db schema
9
+ Adds default configurations
9
10
  Replaces Active Job's adapter in environment configuration
10
11
  Installs bin/jobs binstub to start the supervisor
@@ -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/solid_queue.yml"
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
@@ -0,0 +1,9 @@
1
+ # periodic_cleanup:
2
+ # class: CleanSoftDeletedRecordsJob
3
+ # queue: background
4
+ # args: [ 1000, { batch_size: 500 } ]
5
+ # schedule: every hour
6
+ # periodic_command:
7
+ # command: "SoftDeletedRecord.due.delete_all"
8
+ # priority: 2
9
+ # schedule: at 5am every day
@@ -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", default: Configuration::DEFAULT_CONFIG_FILE_PATH, desc: "Path to config file"
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(load_configuration_from: options["config_file"])
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
- DEFAULT_CONFIG = {
27
- workers: [ WORKER_DEFAULTS ],
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(load_from: nil)
32
- @raw_config = config_from(load_from)
28
+ def initialize(**options)
29
+ @options = options.with_defaults(default_options)
33
30
  end
34
31
 
35
32
  def configured_processes
36
- dispatchers + workers
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 :raw_config
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
- DEFAULT_CONFIG_FILE_PATH = "config/solid_queue.yml"
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
- recurring_tasks = parse_recurring_tasks dispatcher_options[:recurring_tasks]
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 config_from(file_or_hash, env: Rails.env)
64
- load_config_from(file_or_hash).then do |config|
65
- config = config[env.to_sym] ? config[env.to_sym] : config
66
- if (config.keys & DEFAULT_CONFIG.keys).any? then config
67
- else
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 ||= options_from_raw_config(:workers)
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 ||= options_from_raw_config(:dispatchers)
96
+ @dispatchers_options ||= processes_config.fetch(:dispatchers, [])
80
97
  .map { |options| options.dup.symbolize_keys }
81
98
  end
82
99
 
83
- def options_from_raw_config(key)
84
- Array(raw_config[key])
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
- load_config_from_env_location || load_config_from_default_location
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
- raise "Configuration file for Solid Queue not found in #{file}"
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, :recurring_schedule
5
+ attr_accessor :batch_size, :concurrency_maintenance
6
6
 
7
- after_boot :start_concurrency_maintenance, :schedule_recurring_tasks
8
- before_shutdown :stop_concurrency_maintenance, :unschedule_recurring_tasks
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, recurring_schedule: recurring_schedule.task_keys.presence)
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
- info formatted_event(event, action: "Register #{process_kind}", **attributes)
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
- info formatted_event(event, action: "Deregister #{process.kind}", **attributes)
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
@@ -41,9 +41,6 @@ module SolidQueue::Processes
41
41
  raise NotImplementedError
42
42
  end
43
43
 
44
- def shutdown
45
- end
46
-
47
44
  def with_polling_volume
48
45
  SolidQueue.instrument(:polling) do
49
46
  if SolidQueue.silence_polling? && ActiveRecord::Base.logger
@@ -29,11 +29,11 @@ module SolidQueue::Processes
29
29
  end
30
30
 
31
31
  def deregister
32
- process.deregister if registered?
32
+ process&.deregister
33
33
  end
34
34
 
35
35
  def registered?
36
- process&.persisted?
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
- @stopped = true
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
 
@@ -29,7 +29,6 @@ module SolidQueue::Processes
29
29
  %w[ INT TERM ].each do |signal|
30
30
  trap(signal) do
31
31
  stop
32
- interrupt
33
32
  end
34
33
  end
35
34
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueue
4
- class Dispatcher::RecurringSchedule
4
+ class Scheduler::RecurringSchedule
5
5
  include AppExecutor
6
6
 
7
7
  attr_reader :configured_tasks, :scheduled_tasks
@@ -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
@@ -24,7 +24,7 @@ module SolidQueue
24
24
  end
25
25
 
26
26
  def prune_dead_processes
27
- wrap_in_app_executor { SolidQueue::Process.prune }
27
+ wrap_in_app_executor { SolidQueue::Process.prune(excluding: process) }
28
28
  end
29
29
 
30
30
  def fail_orphaned_executions
@@ -6,9 +6,9 @@ module SolidQueue
6
6
  include Maintenance, Signals, Pidfiled
7
7
 
8
8
  class << self
9
- def start(load_configuration_from: nil)
9
+ def start(**options)
10
10
  SolidQueue.supervisor = true
11
- configuration = Configuration.new(load_from: load_configuration_from)
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
- @stopped = true
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
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.8.2"
2
+ VERSION = "1.0.0.beta"
3
3
  end
data/lib/solid_queue.rb CHANGED
@@ -32,8 +32,6 @@ module SolidQueue
32
32
 
33
33
  mattr_accessor :shutdown_timeout, default: 5.seconds
34
34
 
35
- mattr_accessor :enqueue_after_transaction_commit, default: false
36
-
37
35
  mattr_accessor :silence_polling, default: true
38
36
 
39
37
  mattr_accessor :supervisor_pidfile
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.8.2
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-06 00:00:00.000000000 Z
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/solid_queue.yml
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.8.0 from < 0.6.0? You need to upgrade to 0.6.0 first. Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
280
- for upgrade instructions.
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