solid_queue 0.9.0 → 1.0.0

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: b0a582a0c2b6b0025baed737e35b41dbe2dc687b0fbd7a221909b6350321a717
4
- data.tar.gz: 85053c1ab6f8b7d5b66855631a003e789306499eba71d100ae91a32e39757b97
3
+ metadata.gz: 553dc884dd38c24c16196da9cceb6e7b40d02bd3f5c005bc980065232b4b1682
4
+ data.tar.gz: dba6309954b326305526417c05c1a5878a0195d759e8cd6fb6c4453ee61cd6c0
5
5
  SHA512:
6
- metadata.gz: 630b5c980be4b1e152544e46eb38922706b620765ffb1b9c18ff81a9c00a221be2d0ed2f723ecd7d39391a01119785b3be42db408f7e48a6b0afd67ab62088eb
7
- data.tar.gz: 4451b96428dfbaa8213d9fff225dfeaa5826aa10d6ab68151effbba01df866c8c19110e4c6f147a25b70fa34827c6a6c03381b1fb7681934b036791ba4f16faa
6
+ metadata.gz: dc6ad2d8abd513cd9f357a1190c2173c978b46dd800d1b3f4040364cd70c44b5271da7108901c67a2811586d746607eac4f999c8886a56a1942a228f0f3d1d11
7
+ data.tar.gz: 999a90c18c09350609670b29bdd3f5e2596815f0a5edb194b225634b9169eca9efaf3e8efda23cfe346e6947e9477b983bbcae1a8a1adf3c6fc2ae9affd4e2ff
data/README.md CHANGED
@@ -15,7 +15,7 @@ Solid Queue is configured by default in new Rails 8 applications. But if you're
15
15
 
16
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
- 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:
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:
@@ -51,9 +51,12 @@ Now you're ready to start processing jobs by running `bin/jobs` on the server th
51
51
 
52
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.
53
53
 
54
+ **Note**: future changes to the schema will come in the form of regular migrations.
55
+
56
+
54
57
  ### Single database configuration
55
58
 
56
- It's also possibile to use one single database for both production data:
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:
57
60
 
58
61
  1. Copy the contents of `db/queue_schema.rb` into a normal migration and delete `db/queue_schema.rb`
59
62
  2. Remove `config.solid_queue.connects_to` from `production.rb`
@@ -220,6 +223,7 @@ There are several settings that control how Solid Queue works that you can set a
220
223
  ```ruby
221
224
  -> (exception) { Rails.error.report(exception, handled: false) }
222
225
  ```
226
+
223
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.
224
228
 
225
229
  - `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.
@@ -231,7 +235,6 @@ There are several settings that control how Solid Queue works that you can set a
231
235
  - `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`.
232
236
  - `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.
233
237
  - `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.
234
- - `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).
235
238
 
236
239
  ## Errors when enqueuing
237
240
 
@@ -256,7 +259,7 @@ class MyJob < ApplicationJob
256
259
 
257
260
  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).
258
261
 
259
- 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 unsuccesfully, 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.
262
+ 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.
260
263
 
261
264
 
262
265
  For example:
@@ -308,6 +311,33 @@ failed_execution.discard # This will delete the job from the system
308
311
 
309
312
  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.
310
313
 
314
+ ### Error reporting on jobs
315
+
316
+ 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:
317
+
318
+ ```ruby
319
+ # application_job.rb
320
+ class ApplicationJob < ActiveJob::Base
321
+ rescue_from(Exception) do |exception|
322
+ Rails.error.report(exception)
323
+ raise exception
324
+ end
325
+ end
326
+ ```
327
+
328
+ 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`.
329
+
330
+ ```ruby
331
+ # application_mailer.rb
332
+
333
+ class ApplicationMailer < ActionMailer::Base
334
+ ActionMailer::MailDeliveryJob.rescue_from(Exception) do |exception|
335
+ Rails.error.report(exception)
336
+ raise exception
337
+ end
338
+ end
339
+ ```
340
+
311
341
  ## Puma plugin
312
342
 
313
343
  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
@@ -316,6 +346,29 @@ plugin :solid_queue
316
346
  ```
317
347
  to your `puma.rb` configuration.
318
348
 
349
+
350
+ ## Jobs and transactional integrity
351
+ :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.
352
+
353
+ 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.
354
+
355
+ 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.
356
+
357
+ If you set that to `never` but still want to make sure you're not inadvertently on transactional integrity, you can make sure that:
358
+ - 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.
359
+ - 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:
360
+
361
+ ```ruby
362
+ class ApplicationRecord < ActiveRecord::Base
363
+ self.abstract_class = true
364
+
365
+ connects_to database: { writing: :primary, reading: :replica }
366
+ ```
367
+
368
+ ```ruby
369
+ config.solid_queue.connects_to = { database: { writing: :primary, reading: :replica } }
370
+ ```
371
+
319
372
  ## Recurring tasks
320
373
 
321
374
  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:
@@ -327,13 +380,14 @@ bin/jobs --recurring_schedule_file=config/schedule.yml
327
380
  The configuration itself looks like this:
328
381
 
329
382
  ```yml
330
- a_periodic_job:
331
- class: MyJob
332
- args: [ 42, { status: "custom_status" } ]
333
- schedule: every second
334
- a_cleanup_task:
335
- command: "DeletedStuff.clear_all"
336
- schedule: every day at 9am
383
+ production:
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
337
391
  ```
338
392
 
339
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.
@@ -355,6 +409,8 @@ Tasks are enqueued at their corresponding times by the scheduler, and each task
355
409
 
356
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.
357
411
 
412
+ **Note**: a single recurring schedule is supported, so you can have multiple schedulers using the same schedule, but not multiple schedulers using different configurations.
413
+
358
414
  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:
359
415
  ```ruby
360
416
  class MyResqueJob < ApplicationJob
data/UPGRADING.md CHANGED
@@ -1,26 +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
+
1
4
  # Upgrading to version 0.9.x
2
5
  This version has two breaking changes regarding configuration:
3
6
  - The default configuration file has changed from `config/solid_queue.yml` to `config/queue.yml`.
4
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`.
5
8
 
6
9
  # Upgrading to version 0.8.x
7
- *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.
8
11
 
9
12
  # Upgrading to version 0.7.x
10
13
 
11
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.
12
15
 
13
- 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:
14
17
  ```
15
18
  bin/rails generate solid_queue:install
16
19
  ```
17
20
 
18
- Or, if you're using a different database for Solid Queue:
19
-
20
- ```bash
21
- $ bin/rails generate solid_queue:install --database <the_name_of_your_solid_queue_db>
22
- ```
23
-
24
21
 
25
22
  # Upgrading to version 0.6.x
26
23
 
@@ -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)
@@ -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:
@@ -1,9 +1,10 @@
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
1
+ # production:
2
+ # periodic_cleanup:
3
+ # class: CleanSoftDeletedRecordsJob
4
+ # queue: background
5
+ # args: [ 1000, { batch_size: 500 } ]
6
+ # schedule: every hour
7
+ # periodic_command:
8
+ # command: "SoftDeletedRecord.due.delete_all"
9
+ # priority: 2
10
+ # schedule: at 5am every day
@@ -1,4 +1,4 @@
1
- ActiveRecord::Schema[7.1].define(version: 2024_09_04_193154) do
1
+ ActiveRecord::Schema[7.1].define(version: 1) do
2
2
  create_table "solid_queue_blocked_executions", force: :cascade do |t|
3
3
  t.bigint "job_id", null: false
4
4
  t.string "queue_name", null: false
@@ -5,16 +5,15 @@ require "thor"
5
5
  module SolidQueue
6
6
  class Cli < Thor
7
7
  class_option :config_file, type: :string, aliases: "-c",
8
- default: Configuration::DEFAULT_CONFIG_FILE_PATH,
9
- desc: "Path to config file",
8
+ desc: "Path to config file (default: #{Configuration::DEFAULT_CONFIG_FILE_PATH}).",
10
9
  banner: "SOLID_QUEUE_CONFIG"
11
10
 
12
11
  class_option :recurring_schedule_file, type: :string,
13
- default: Configuration::DEFAULT_RECURRING_SCHEDULE_FILE_PATH,
14
- desc: "Path to recurring schedule definition",
12
+ desc: "Path to recurring schedule definition (default: #{Configuration::DEFAULT_RECURRING_SCHEDULE_FILE_PATH}).",
15
13
  banner: "SOLID_QUEUE_RECURRING_SCHEDULE"
16
14
 
17
- class_option :skip_recurring, type: :boolean, default: false
15
+ class_option :skip_recurring, type: :boolean, default: false,
16
+ desc: "Whether to skip recurring tasks scheduling"
18
17
 
19
18
  def self.exit_on_failure?
20
19
  true
@@ -111,7 +111,7 @@ module SolidQueue
111
111
  end
112
112
 
113
113
  def recurring_tasks_config
114
- @recurring_tasks ||= config_from options[:recurring_schedule_file]
114
+ @recurring_tasks_config ||= config_from options[:recurring_schedule_file]
115
115
  end
116
116
 
117
117
 
@@ -53,7 +53,7 @@ module SolidQueue::Processes
53
53
  end
54
54
 
55
55
  def heartbeat
56
- process.with_lock { process.heartbeat }
56
+ process.heartbeat
57
57
  rescue ActiveRecord::RecordNotFound
58
58
  self.process = nil
59
59
  wake_up
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.9.0"
2
+ VERSION = "1.0.0"
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.9.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rosa Gutierrez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-11 00:00:00.000000000 Z
11
+ date: 2024-09-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord