solid_queue 0.8.2 → 0.9.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: b975d1366456d8f53686e0cee4f14e35dfc5461b206de7553818a109f6ae0088
4
- data.tar.gz: 3d1084ecd73a3cd5c557d89b9edf36bac7db5c1f9ce2e995811c31370c261b6b
3
+ metadata.gz: b0a582a0c2b6b0025baed737e35b41dbe2dc687b0fbd7a221909b6350321a717
4
+ data.tar.gz: 85053c1ab6f8b7d5b66855631a003e789306499eba71d100ae91a32e39757b97
5
5
  SHA512:
6
- metadata.gz: e06a86af9d282440726a2dfd81d03637d093e7a12fca7f43be3a1f28312d9117c90ce778a7c04923eb5a57feb2909721d1da9c32f0d5b640dc3c09cf3a3c98db
7
- data.tar.gz: 888f76de27a21e99cc64835b005a140b555b812a06f32b7c7588c5b423e8f84301760c653bcfa2a4020def4d6aa3f8c5464684277bf26833355e4cc82cf684d5
6
+ metadata.gz: 630b5c980be4b1e152544e46eb38922706b620765ffb1b9c18ff81a9c00a221be2d0ed2f723ecd7d39391a01119785b3be42db408f7e48a6b0afd67ab62088eb
7
+ data.tar.gz: 4451b96428dfbaa8213d9fff225dfeaa5826aa10d6ab68151effbba01df866c8c19110e4c6f147a25b70fa34827c6a6c03381b1fb7681934b036791ba4f16faa
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,24 @@ 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
+ ### Single database configuration
55
+
56
+ It's also possibile to use one single database for both production data:
57
+
58
+ 1. Copy the contents of `db/queue_schema.rb` into a normal migration and delete `db/queue_schema.rb`
59
+ 2. Remove `config.solid_queue.connects_to` from `production.rb`
60
+ 3. Migrate your database. You are ready to run `bin/jobs`
61
+
62
+ You won't have multiple databases, so `database.yml` doesn't need to have primary and queue database.
63
+
52
64
  ## Incremental adoption
53
65
 
54
66
  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 +73,31 @@ class MyJob < ApplicationJob
61
73
  # ...
62
74
  end
63
75
  ```
76
+
64
77
  ## High performance requirements
65
78
 
66
79
  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
80
 
68
81
  ## Configuration
69
82
 
70
- ### Workers and dispatchers
83
+ ### Workers, dispatchers and scheduler
71
84
 
72
- We have three types of actors in Solid Queue:
85
+ We have several types of actors in Solid Queue:
73
86
  - _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).
87
+ - _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).
88
+ - The _scheduler_ manages [recurring tasks](#recurring-tasks), enqueuing jobs for them when they're due.
75
89
  - The _supervisor_ runs workers and dispatchers according to the configuration, controls their heartbeats, and stops and starts them when needed.
76
90
 
77
- Solid Queue's supervisor will fork a separate process for each supervised worker/dispatcher.
91
+ Solid Queue's supervisor will fork a separate process for each supervised worker/dispatcher/scheduler.
78
92
 
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:
93
+ 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:
94
+
95
+ ```
96
+ bin/jobs -c config/calendar.yml
97
+ ```
98
+
99
+
100
+ This is what this configuration looks like:
80
101
 
81
102
  ```yml
82
103
  production:
@@ -105,6 +126,7 @@ production:
105
126
  ```
106
127
  the supervisor will run 1 dispatcher and no workers.
107
128
 
129
+
108
130
  Here's an overview of the different options:
109
131
 
110
132
  - `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 +149,7 @@ Here's an overview of the different options:
127
149
  - `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
150
  - `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
151
  - `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.
152
+
131
153
 
132
154
  ### Queue order and priorities
133
155
 
@@ -234,6 +256,9 @@ class MyJob < ApplicationJob
234
256
 
235
257
  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
258
 
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.
260
+
261
+
237
262
  For example:
238
263
  ```ruby
239
264
  class DeliverAnnouncementToContactJob < ApplicationJob
@@ -242,7 +267,7 @@ class DeliverAnnouncementToContactJob < ApplicationJob
242
267
  def perform(contact)
243
268
  # ...
244
269
  ```
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.
270
+ 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
271
 
247
272
  Let's see another example using `group`:
248
273
 
@@ -268,7 +293,7 @@ Note that the `duration` setting depends indirectly on the value for `concurrenc
268
293
 
269
294
  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
295
 
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.
296
+ 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
297
 
273
298
  ## Failed jobs and retries
274
299
 
@@ -293,27 +318,42 @@ to your `puma.rb` configuration.
293
318
 
294
319
  ## Recurring tasks
295
320
 
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:
321
+ 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:
322
+
323
+ ```
324
+ bin/jobs --recurring_schedule_file=config/schedule.yml
325
+ ```
326
+
327
+ The configuration itself looks like this:
328
+
297
329
  ```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
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
306
337
  ```
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.
338
+
339
+ 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.
340
+
341
+ 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:
342
+ - `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
343
 
309
344
  The job in the example configuration above will be enqueued every second as:
310
345
  ```ruby
311
346
  MyJob.perform_later(42, status: "custom_status")
312
347
  ```
313
348
 
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).
349
+ - `queue`: a different queue to be used when enqueuing the job. If none, the queue set up for the job class.
350
+
351
+ - `priority`: a numeric priority value to be used when enqueuing the job.
352
+
315
353
 
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.
354
+ 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).
355
+
356
+ 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
357
 
318
358
  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
359
  ```ruby
@@ -328,13 +368,12 @@ end
328
368
 
329
369
  You can still configure this in Solid Queue:
330
370
  ```yml
331
- dispatchers:
332
- - recurring_tasks:
333
- my_periodic_resque_job:
334
- class: MyResqueJob
335
- args: 22
336
- schedule: "*/5 * * * *"
371
+ my_periodic_resque_job:
372
+ class: MyResqueJob
373
+ args: 22
374
+ schedule: "*/5 * * * *"
337
375
  ```
376
+
338
377
  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
378
 
340
379
  ## 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,3 +1,8 @@
1
+ # Upgrading to version 0.9.x
2
+ This version has two breaking changes regarding configuration:
3
+ - The default configuration file has changed from `config/solid_queue.yml` to `config/queue.yml`.
4
+ - 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
+
1
6
  # Upgrading to version 0.8.x
2
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.
3
8
 
@@ -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)
@@ -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
@@ -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,17 @@ 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
+ default: Configuration::DEFAULT_CONFIG_FILE_PATH,
9
+ desc: "Path to config file",
10
+ banner: "SOLID_QUEUE_CONFIG"
11
+
12
+ class_option :recurring_schedule_file, type: :string,
13
+ default: Configuration::DEFAULT_RECURRING_SCHEDULE_FILE_PATH,
14
+ desc: "Path to recurring schedule definition",
15
+ banner: "SOLID_QUEUE_RECURRING_SCHEDULE"
16
+
17
+ class_option :skip_recurring, type: :boolean, default: false
8
18
 
9
19
  def self.exit_on_failure?
10
20
  true
@@ -14,7 +24,7 @@ module SolidQueue
14
24
  default_command :start
15
25
 
16
26
  def start
17
- SolidQueue::Supervisor.start(load_configuration_from: options["config_file"])
27
+ SolidQueue::Supervisor.start(**options.symbolize_keys)
18
28
  end
19
29
  end
20
30
  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
@@ -53,7 +53,10 @@ module SolidQueue::Processes
53
53
  end
54
54
 
55
55
  def heartbeat
56
- process.heartbeat
56
+ process.with_lock { 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 = "0.9.0"
3
3
  end
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: 0.9.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-06 00:00:00.000000000 Z
11
+ date: 2024-09-11 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