solid_queue 0.6.1 → 0.7.1

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: c2734f6cbcc795345207dc0cf221bd2744f2f98449da8b0edfc8175f3bbd7869
4
- data.tar.gz: c4b4c2eca7dcb93e86a2f69d220ba1b9f8817c1d52a7cd1ff137ed5ec84e50f8
3
+ metadata.gz: a7234dc4430998648196bf2d3905b66bb85e265c2393121a7c5343fed36b1996
4
+ data.tar.gz: 216a0918e29194e6d11fe4bf365183228127f8390658a11ace329523ad41468c
5
5
  SHA512:
6
- metadata.gz: 9d09b58e43c4bc19ad2ab89d10072b36b53a449e9602f5c5a8fc7b637e91d8472eb70f417148657e6b146e5cd1034025afdbe6d39fa2b9fc82ce6bba3997e57f
7
- data.tar.gz: 27168fd2216fbca4e9b19677a7885b23663012d8aed7bf54c25be8335bd528a733b55790e662a30815b9ba24d824390372eff24ed0b9f72947b6ff4feeec17c5
6
+ metadata.gz: a080aedf20f39940d8c25e8a14628811801b8fd4832fbcb926beecd0d1ce4c694ec8d80b608656f9de80cc8cc69b525f62d4fe7bc7258408aa6d6af62292d4fd
7
+ data.tar.gz: 66d24c1bb1c7cb1b9afbcf8a0b667df624dd3f47cea92887fbcd69d93b1832af3c44afb570d1cfb459c37e2b3c163900e0dd2febb067ee9179b4c49514e4b359
data/README.md CHANGED
@@ -2,7 +2,7 @@
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`). _Improvements to logging and instrumentation, a better CLI tool, a way to run within an existing process in "async" mode, and some way of specifying unique jobs are coming very soon._
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`).
6
6
 
7
7
  Solid Queue can be used with SQL databases such as MySQL, PostgreSQL or SQLite, and it leverages the `FOR UPDATE SKIP LOCKED` clause, if available, to avoid blocking and waiting on locks when polling jobs. It relies on Active Job for retries, discarding, error handling, serialization, or delays, and it's compatible with Ruby on Rails multi-threading.
8
8
 
@@ -31,9 +31,9 @@ $ bin/rails generate solid_queue:install
31
31
 
32
32
  This will set `solid_queue` as the Active Job's adapter in production, and will copy the required migration over to your app.
33
33
 
34
- Alternatively, you can add only the migration to your app:
34
+ Alternatively, you can skip setting the Active Job's adapter with:
35
35
  ```bash
36
- $ bin/rails solid_queue:install:migrations
36
+ $ bin/rails generate solid_queue:install --skip_adapter
37
37
  ```
38
38
 
39
39
  And set Solid Queue as your Active Job's queue backend manually, in your environment config:
@@ -42,7 +42,7 @@ And set Solid Queue as your Active Job's queue backend manually, in your environ
42
42
  config.active_job.queue_adapter = :solid_queue
43
43
  ```
44
44
 
45
- Alternatively, you can set only specific jobs to use Solid Queue as their backend if you're migrating from another adapter and want to move jobs progressively:
45
+ Or you can set only specific jobs to use Solid Queue as their backend if you're migrating from another adapter and want to move jobs progressively:
46
46
 
47
47
  ```ruby
48
48
  # app/jobs/my_job.rb
@@ -59,14 +59,14 @@ Finally, you need to run the migrations:
59
59
  $ bin/rails db:migrate
60
60
  ```
61
61
 
62
- After this, you'll be ready to enqueue jobs using Solid Queue, but you need to start Solid Queue's supervisor to run them.
62
+ After this, you'll be ready to enqueue jobs using Solid Queue, but you need to start Solid Queue's supervisor to run them. You can use the provided binstub:`
63
63
  ```bash
64
- $ bundle exec rake solid_queue:start
64
+ $ bin/jobs
65
65
  ```
66
66
 
67
67
  This will start processing jobs in all queues using the default configuration. See [below](#configuration) to learn more about configuring Solid Queue.
68
68
 
69
- 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 `bundle exec rake solid_queue:start` 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.
69
+ 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.
70
70
 
71
71
  ## Requirements
72
72
  Besides Rails 7.1, Solid Queue works best 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.
@@ -80,7 +80,7 @@ We have three types of actors in Solid Queue:
80
80
  - _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).
81
81
  - The _supervisor_ runs workers and dispatchers according to the configuration, controls their heartbeats, and stops and starts them when needed.
82
82
 
83
- By default, Solid Queue runs in `fork` mode. This means the supervisor will fork a separate process for each supervised worker/dispatcher. There's also an `async` mode where each worker and dispatcher will be run as a thread of the supervisor process. This can be used with [the provided Puma plugin](#puma-plugin)
83
+ Solid Queue's supervisor will fork a separate process for each supervised worker/dispatcher.
84
84
 
85
85
  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:
86
86
 
@@ -131,7 +131,7 @@ Here's an overview of the different options:
131
131
 
132
132
  Finally, you can combine prefixes with exact names, like `[ staging*, background ]`, and the behaviour with respect to order will be the same as with only exact names.
133
133
  - `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.
134
- - `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. **Note**: this option will be ignored if [running in `async` mode](#running-as-a-fork-or-asynchronously).
134
+ - `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.
135
135
  - `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.
136
136
  - `recurring_tasks`: a list of recurring tasks the dispatcher will manage. Read more details about this one in the [Recurring tasks](#recurring-tasks) section.
137
137
 
@@ -194,13 +194,13 @@ development:
194
194
  # ...
195
195
  ```
196
196
 
197
- Install migrations and specify the dedicated database name with the `DATABASE` option. This will create the Solid Queue migration files in a separate directory, matching the value provided in `migrations_paths` in `config/database.yml`.
197
+ Install migrations and specify the dedicated database name with the `--database` option. This will create the Solid Queue migration files in a separate directory, matching the value provided in `migrations_paths` in `config/database.yml`.
198
198
 
199
199
  ```bash
200
- $ bin/rails solid_queue:install:migrations DATABASE=solid_queue
200
+ $ bin/rails g solid_queue:install --database solid_queue
201
201
  ```
202
202
 
203
- Note: If you've already run the solid queue install command (`bin/rails generate solid_queue:install`), the migration files will have already been generated under the primary database's `db/migrate/` directory. You can remove these files and keep the ones generated by the database-specific migration installation above.
203
+ Note: If you've already run the solid queue install command (`bin/rails generate solid_queue:install`) without a `--database` option, the migration files will have already been generated under the primary database's `db/migrate/` directory. You can remove these files and keep the ones generated by the database-specific migration installation above.
204
204
 
205
205
  Finally, run the migrations:
206
206
 
@@ -208,17 +208,47 @@ Finally, run the migrations:
208
208
  $ bin/rails db:migrate
209
209
  ```
210
210
 
211
+ ## Lifecycle hooks
212
+
213
+ In Solid queue, you can hook into two different points in the supervisor's life:
214
+ - `start`: after the supervisor has finished booting and right before it forks workers and dispatchers.
215
+ - `stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown.
216
+
217
+ And into two different points in a worker's life:
218
+ - `worker_start`: after the worker has finished booting and right before it starts the polling loop.
219
+ - `worker_stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown (which is just `exit!`).
220
+
221
+ You can use the following methods with a block to do this:
222
+ ```ruby
223
+ SolidQueue.on_start
224
+ SolidQueue.on_stop
225
+
226
+ SolidQueue.on_worker_start
227
+ SolidQueue.on_worker_stop
228
+ ```
229
+
230
+ For example:
231
+ ```ruby
232
+ SolidQueue.on_start { start_metrics_server }
233
+ SolidQueue.on_stop { stop_metrics_server }
234
+ ```
235
+
236
+ These can be called several times to add multiple hooks, but it needs to happen before Solid Queue is started. An initializer would be a good place to do this.
237
+
238
+
211
239
  ### Other configuration settings
212
240
  _Note_: The settings in this section should be set in your `config/application.rb` or your environment config like this: `config.solid_queue.silence_polling = true`
213
241
 
214
242
  There are several settings that control how Solid Queue works that you can set as well:
215
243
  - `logger`: the logger you want Solid Queue to use. Defaults to the app logger.
216
244
  - `app_executor`: the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
217
- - `on_thread_error`: custom lambda/Proc to call when there's an error within a thread that takes the exception raised as argument. Defaults to
245
+ - `on_thread_error`: custom lambda/Proc to call when there's an error within a Solid Queue thread that takes the exception raised as argument. Defaults to
218
246
 
219
247
  ```ruby
220
248
  -> (exception) { Rails.error.report(exception, handled: false) }
221
249
  ```
250
+ **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.
251
+
222
252
  - `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.
223
253
  - `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
224
254
  - `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
@@ -283,6 +313,8 @@ In this case, if we have a `Box::MovePostingsByContactToDesignatedBoxJob` job en
283
313
 
284
314
  Note that the `duration` setting depends indirectly on the value for `concurrency_maintenance_interval` that you set for your dispatcher(s), as that'd be the frequency with which blocked jobs are checked and unblocked. In general, you should set `duration` in a way that all your jobs would finish well under that duration and think of the concurrency maintenance task as a failsafe in case something goes wrong.
285
315
 
316
+ 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.
317
+
286
318
  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.
287
319
 
288
320
  ## Failed jobs and retries
@@ -305,18 +337,6 @@ plugin :solid_queue
305
337
  ```
306
338
  to your `puma.rb` configuration.
307
339
 
308
- ### Running as a fork or asynchronously
309
-
310
- By default, the Puma plugin will fork additional processes for each worker and dispatcher so that they run in different processes. This provides the best isolation and performance, but can have additional memory usage.
311
-
312
- Alternatively, workers and dispatchers can be run within the same Puma process(s). To do so just configure the plugin as:
313
-
314
- ```ruby
315
- plugin :solid_queue
316
- solid_queue_mode :async
317
- ```
318
-
319
- Note that in this case, the `processes` configuration option will be ignored.
320
340
 
321
341
  ## Jobs and transactional integrity
322
342
  :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. 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.
data/UPGRADING.md ADDED
@@ -0,0 +1,102 @@
1
+ # Upgrading to version 0.7.x
2
+
3
+ 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. It includes also a minor migration.
4
+
5
+ To install both the binstub `bin/jobs` and the migration, you can just run
6
+ ```
7
+ bin/rails generate solid_queue:install
8
+ ```
9
+
10
+ Or, if you're using a different database for Solid Queue:
11
+
12
+ ```bash
13
+ $ bin/rails generate solid_queue:install --database <the_name_of_your_solid_queue_db>
14
+ ```
15
+
16
+
17
+ # Upgrading to version 0.6.x
18
+
19
+ ## New migration in 3 steps
20
+ This version adds two new migrations to modify the `solid_queue_processes` table. The goal of that migration is to add a new column that needs to be `NOT NULL`. This needs to be done with two migrations and the following steps to ensure it happens without downtime and with new processes being able to register just fine:
21
+ 1. Run the first migration that adds the new column, nullable
22
+ 2. Deploy the updated Solid Queue code that uses this column
23
+ 2. Run the second migration. This migration does two things:
24
+ - Backfill existing rows that would have the column as NULL
25
+ - Make the column not nullable and add a new index
26
+
27
+ Besides, it adds another migration with no effects to the `solid_queue_recurring_tasks` table. This one can be run just fine whenever, as the column affected is not used.
28
+
29
+ To install the migrations:
30
+ ```bash
31
+ $ bin/rails solid_queue:install:migrations
32
+ ```
33
+
34
+ Or, if you're using a different database for Solid Queue:
35
+
36
+ ```bash
37
+ $ bin/rails solid_queue:install:migrations DATABASE=<the_name_of_your_solid_queue_db>
38
+ ```
39
+
40
+ And then follow the steps above, running first one, then deploying the code, then running the second one.
41
+
42
+ ## New behaviour when workers are killed
43
+ From this version onwards, when a worker is killed and the supervisor can detect that, it'll fail in-progress jobs claimed by that worker. For this to work correctly, you need to run the above migration and ensure you restart any supervisors you'd have.
44
+
45
+
46
+ # Upgrading to version 0.5.x
47
+ This version includes a new migration to improve recurring tasks. To install it, just run:
48
+
49
+ ```bash
50
+ $ bin/rails solid_queue:install:migrations
51
+ ```
52
+
53
+ Or, if you're using a different database for Solid Queue:
54
+
55
+ ```bash
56
+ $ bin/rails solid_queue:install:migrations DATABASE=<the_name_of_your_solid_queue_db>
57
+ ```
58
+
59
+ And then run the migrations.
60
+
61
+
62
+ # Upgrading to version 0.4.x
63
+ This version introduced an _async_ mode (this mode has been removed in version 0.7.0) to run the supervisor and have all workers and dispatchers run as part of the same process as the supervisor, instead of separate, forked, processes. Together with this, we introduced some changes in how the supervisor is started. Prior this change, you could choose whether you wanted to run workers, dispatchers or both, by starting Solid Queue as `solid_queue:work` or `solid_queue:dispatch`. From version 0.4.0, the only option available is:
64
+
65
+ ```
66
+ $ bundle exec rake solid_queue:start
67
+ ```
68
+ Whether the supervisor starts workers, dispatchers or both will depend on your configuration. For example, if you don't configure any dispatchers, only workers will be started. That is, with this configuration:
69
+
70
+ ```yml
71
+ production:
72
+ workers:
73
+ - queues: [ real_time, background ]
74
+ threads: 5
75
+ polling_interval: 0.1
76
+ processes: 3
77
+ ```
78
+ the supervisor will run 3 workers, each one with 5 threads, and no supervisors. With this configuration:
79
+ ```yml
80
+ production:
81
+ dispatchers:
82
+ - polling_interval: 1
83
+ batch_size: 500
84
+ concurrency_maintenance_interval: 300
85
+ ```
86
+ the supervisor will run 1 dispatcher and no workers.
87
+
88
+
89
+ # Upgrading to version 0.3.x
90
+ This version introduced support for [recurring (cron-style) jobs](https://github.com/rails/solid_queue/blob/main/README.md#recurring-tasks), and it needs a new DB migration for it. To install it, just run:
91
+
92
+ ```bash
93
+ $ bin/rails solid_queue:install:migrations
94
+ ```
95
+
96
+ Or, if you're using a different database for Solid Queue:
97
+
98
+ ```bash
99
+ $ bin/rails solid_queue:install:migrations DATABASE=<the_name_of_your_solid_queue_db>
100
+ ```
101
+
102
+ And then run the migrations.
@@ -7,3 +7,4 @@ Example:
7
7
  This will perform the following:
8
8
  Installs solid_queue migrations
9
9
  Replaces Active Job's adapter in environment configuration
10
+ Installs bin/jobs binstub to start the supervisor
@@ -3,19 +3,33 @@
3
3
  class SolidQueue::InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path("templates", __dir__)
5
5
 
6
- class_option :skip_migrations, type: :boolean, default: nil, desc: "Skip migrations"
6
+ class_option :skip_adapter, type: :boolean, default: nil, desc: "Skip setting Solid Queue as the Active Job's adapter"
7
+ class_option :database, type: :string, default: nil, desc: "The database to use for migrations, if different from the primary one."
7
8
 
8
9
  def add_solid_queue
9
- if (env_config = Pathname(destination_root).join("config/environments/production.rb")).exist?
10
- gsub_file env_config, /(# )?config\.active_job\.queue_adapter\s+=.*/, "config.active_job.queue_adapter = :solid_queue"
10
+ unless options[:skip_adapter]
11
+ if (env_config = Pathname(destination_root).join("config/environments/production.rb")).exist?
12
+ say "Setting solid_queue as Active Job's queue adapter"
13
+ gsub_file env_config, /(# )?config\.active_job\.queue_adapter\s+=.*/, "config.active_job.queue_adapter = :solid_queue"
14
+ end
11
15
  end
12
16
 
13
- copy_file "config.yml", "config/solid_queue.yml"
17
+ if File.exist?("config/solid_queue.yml")
18
+ say "Skipping sample configuration as config/solid_queue.yml exists"
19
+ else
20
+ say "Copying sample configuration"
21
+ copy_file "config.yml", "config/solid_queue.yml"
22
+ end
23
+
24
+ say "Copying binstub"
25
+ copy_file "jobs", "bin/jobs"
26
+ chmod "bin/jobs", 0755 & ~File.umask, verbose: false
14
27
  end
15
28
 
16
29
  def create_migrations
17
- unless options[:skip_migrations]
18
- rails_command "railties:install:migrations FROM=solid_queue", inline: true
19
- end
30
+ say "Installing database migrations"
31
+ arguments = [ "FROM=solid_queue" ]
32
+ arguments << "DATABASE=#{options[:database]}" if options[:database].present?
33
+ rails_command "railties:install:migrations #{arguments.join(" ")}", inline: true
20
34
  end
21
35
  end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../config/environment"
4
+ require "solid_queue/cli"
5
+
6
+ SolidQueue::Cli.start(ARGV)
@@ -1,13 +1,5 @@
1
1
  require "puma/plugin"
2
2
 
3
- module Puma
4
- class DSL
5
- def solid_queue_mode(mode = :fork)
6
- @options[:solid_queue_mode] = mode.to_sym
7
- end
8
- end
9
- end
10
-
11
3
  Puma::Plugin.create do
12
4
  attr_reader :puma_pid, :solid_queue_pid, :log_writer, :solid_queue_supervisor
13
5
 
@@ -15,36 +7,22 @@ Puma::Plugin.create do
15
7
  @log_writer = launcher.log_writer
16
8
  @puma_pid = $$
17
9
 
18
- if launcher.options[:solid_queue_mode] == :async
19
- start_async(launcher)
20
- else
21
- start_forked(launcher)
10
+ in_background do
11
+ monitor_solid_queue
22
12
  end
23
- end
24
13
 
25
- private
26
- def start_forked(launcher)
27
- in_background do
28
- monitor_solid_queue
14
+ launcher.events.on_booted do
15
+ @solid_queue_pid = fork do
16
+ Thread.new { monitor_puma }
17
+ SolidQueue::Supervisor.start
29
18
  end
30
-
31
- launcher.events.on_booted do
32
- @solid_queue_pid = fork do
33
- Thread.new { monitor_puma }
34
- SolidQueue::Supervisor.start(mode: :fork)
35
- end
36
- end
37
-
38
- launcher.events.on_stopped { stop_solid_queue }
39
- launcher.events.on_restart { stop_solid_queue }
40
19
  end
41
20
 
42
- def start_async(launcher)
43
- launcher.events.on_booted { @solid_queue_supervisor = SolidQueue::Supervisor.start(mode: :async) }
44
- launcher.events.on_stopped { solid_queue_supervisor.stop }
45
- launcher.events.on_restart { solid_queue_supervisor.stop; solid_queue_supervisor.start }
46
- end
21
+ launcher.events.on_stopped { stop_solid_queue }
22
+ launcher.events.on_restart { stop_solid_queue }
23
+ end
47
24
 
25
+ private
48
26
  def stop_solid_queue
49
27
  Process.waitpid(solid_queue_pid, Process::WNOHANG)
50
28
  log "Stopping Solid Queue..."
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module SolidQueue
6
+ class Cli < Thor
7
+ class_option :config_file, type: :string, aliases: "-c", default: Configuration::DEFAULT_CONFIG_FILE_PATH, desc: "Path to config file"
8
+
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
13
+ desc :start, "Starts Solid Queue supervisor to dispatch and perform enqueued jobs. Default command."
14
+ default_command :start
15
+
16
+ def start
17
+ SolidQueue::Supervisor.start(load_configuration_from: options["config_file"])
18
+ end
19
+ end
20
+ end
@@ -28,8 +28,7 @@ module SolidQueue
28
28
  dispatchers: [ DISPATCHER_DEFAULTS ]
29
29
  }
30
30
 
31
- def initialize(mode: :fork, load_from: nil)
32
- @mode = mode.to_s.inquiry
31
+ def initialize(load_from: nil)
33
32
  @raw_config = config_from(load_from)
34
33
  end
35
34
 
@@ -43,17 +42,13 @@ module SolidQueue
43
42
  end
44
43
 
45
44
  private
46
- attr_reader :raw_config, :mode
45
+ attr_reader :raw_config
47
46
 
48
47
  DEFAULT_CONFIG_FILE_PATH = "config/solid_queue.yml"
49
48
 
50
49
  def workers
51
50
  workers_options.flat_map do |worker_options|
52
- processes = if mode.fork?
53
- worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
54
- else
55
- WORKER_DEFAULTS[:processes]
56
- end
51
+ processes = worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
57
52
  processes.times.map { Process.new(:worker, worker_options.with_defaults(WORKER_DEFAULTS)) }
58
53
  end
59
54
  end
@@ -55,7 +55,7 @@ module SolidQueue
55
55
  end
56
56
 
57
57
  def set_procline
58
- procline "waiting"
58
+ procline "dispatching every #{polling_interval.seconds} seconds"
59
59
  end
60
60
  end
61
61
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ module LifecycleHooks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ mattr_reader :lifecycle_hooks, default: { start: [], stop: [] }
9
+ end
10
+
11
+ class_methods do
12
+ def on_start(&block)
13
+ self.lifecycle_hooks[:start] << block
14
+ end
15
+
16
+ def on_stop(&block)
17
+ self.lifecycle_hooks[:stop] << block
18
+ end
19
+
20
+ def clear_hooks
21
+ self.lifecycle_hooks[:start] = []
22
+ self.lifecycle_hooks[:stop] = []
23
+ end
24
+ end
25
+
26
+ private
27
+ def run_start_hooks
28
+ run_hooks_for :start
29
+ end
30
+
31
+ def run_stop_hooks
32
+ run_hooks_for :stop
33
+ end
34
+
35
+ def run_hooks_for(event)
36
+ self.class.lifecycle_hooks.fetch(event, []).each do |block|
37
+ block.call
38
+ rescue Exception => exception
39
+ handle_thread_error(exception)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -4,10 +4,13 @@ module SolidQueue
4
4
  module Processes
5
5
  class ProcessExitError < RuntimeError
6
6
  def initialize(status)
7
- message = case
8
- when status.exitstatus.present? then "Process pid=#{status.pid} exited with status #{status. exitstatus}"
9
- when status.signaled? then "Process pid=#{status.pid} received unhandled signal #{status. termsig}"
10
- else "Process pid=#{status.pid} exited unexpectedly"
7
+ message = "Process pid=#{status.pid} exited unexpectedly."
8
+ if status.exitstatus.present?
9
+ message += " Exited with status #{status.exitstatus}."
10
+ end
11
+
12
+ if status.signaled?
13
+ message += " Received unhandled signal #{status.termsig}."
11
14
  end
12
15
 
13
16
  super(message)
@@ -7,11 +7,7 @@ module SolidQueue::Processes
7
7
  attr_writer :mode
8
8
 
9
9
  def start
10
- @stopped = false
11
-
12
- SolidQueue.instrument(:start_process, process: self) do
13
- run_callbacks(:boot) { boot }
14
- end
10
+ boot
15
11
 
16
12
  if running_async?
17
13
  @thread = create_thread { run }
@@ -25,10 +21,6 @@ module SolidQueue::Processes
25
21
  @thread&.join
26
22
  end
27
23
 
28
- def alive?
29
- !running_async? || @thread.alive?
30
- end
31
-
32
24
  private
33
25
  DEFAULT_MODE = :async
34
26
 
@@ -37,9 +29,15 @@ module SolidQueue::Processes
37
29
  end
38
30
 
39
31
  def boot
40
- if running_as_fork?
41
- register_signal_handlers
42
- set_procline
32
+ SolidQueue.instrument(:start_process, process: self) do
33
+ run_callbacks(:boot) do
34
+ @stopped = false
35
+
36
+ if running_as_fork?
37
+ register_signal_handlers
38
+ set_procline
39
+ end
40
+ end
43
41
  end
44
42
  end
45
43
 
@@ -2,16 +2,16 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Supervisor < Processes::Base
5
- include Maintenance
5
+ include LifecycleHooks
6
+ include Maintenance, Signals, Pidfiled
6
7
 
7
8
  class << self
8
- def start(mode: :fork, load_configuration_from: nil)
9
+ def start(load_configuration_from: nil)
9
10
  SolidQueue.supervisor = true
10
- configuration = Configuration.new(mode: mode, load_from: load_configuration_from)
11
+ configuration = Configuration.new(load_from: load_configuration_from)
11
12
 
12
13
  if configuration.configured_processes.any?
13
- klass = mode == :fork ? ForkSupervisor : AsyncSupervisor
14
- klass.new(configuration).tap(&:start)
14
+ new(configuration).tap(&:start)
15
15
  else
16
16
  abort "No workers or processed configured. Exiting..."
17
17
  end
@@ -20,11 +20,15 @@ module SolidQueue
20
20
 
21
21
  def initialize(configuration)
22
22
  @configuration = configuration
23
+ @forks = {}
24
+ @configured_processes = {}
25
+
23
26
  super
24
27
  end
25
28
 
26
29
  def start
27
30
  boot
31
+ run_start_hooks
28
32
 
29
33
  start_processes
30
34
  launch_maintenance_task
@@ -34,10 +38,11 @@ module SolidQueue
34
38
 
35
39
  def stop
36
40
  @stopped = true
41
+ run_stop_hooks
37
42
  end
38
43
 
39
44
  private
40
- attr_reader :configuration
45
+ attr_reader :configuration, :forks, :configured_processes
41
46
 
42
47
  def boot
43
48
  SolidQueue.instrument(:start_process, process: self) do
@@ -52,15 +57,63 @@ module SolidQueue
52
57
  configuration.configured_processes.each { |configured_process| start_process(configured_process) }
53
58
  end
54
59
 
60
+ def supervise
61
+ loop do
62
+ break if stopped?
63
+
64
+ set_procline
65
+ process_signal_queue
66
+
67
+ unless stopped?
68
+ reap_and_replace_terminated_forks
69
+ interruptible_sleep(1.second)
70
+ end
71
+ end
72
+ ensure
73
+ shutdown
74
+ end
75
+
76
+ def start_process(configured_process)
77
+ process_instance = configured_process.instantiate.tap do |instance|
78
+ instance.supervised_by process
79
+ instance.mode = :fork
80
+ end
81
+
82
+ pid = fork do
83
+ process_instance.start
84
+ end
85
+
86
+ configured_processes[pid] = configured_process
87
+ forks[pid] = process_instance
88
+ end
89
+
55
90
  def stopped?
56
91
  @stopped
57
92
  end
58
93
 
59
- def supervise
94
+ def set_procline
95
+ procline "supervising #{supervised_processes.join(", ")}"
60
96
  end
61
97
 
62
- def start_process(configured_process)
63
- raise NotImplementedError
98
+ def terminate_gracefully
99
+ SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do |payload|
100
+ term_forks
101
+
102
+ Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
103
+ reap_terminated_forks
104
+ end
105
+
106
+ unless all_forks_terminated?
107
+ payload[:shutdown_timeout_exceeded] = true
108
+ terminate_immediately
109
+ end
110
+ end
111
+ end
112
+
113
+ def terminate_immediately
114
+ SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do
115
+ quit_forks
116
+ end
64
117
  end
65
118
 
66
119
  def shutdown
@@ -74,5 +127,63 @@ module SolidQueue
74
127
  def sync_std_streams
75
128
  STDOUT.sync = STDERR.sync = true
76
129
  end
130
+
131
+ def supervised_processes
132
+ forks.keys
133
+ end
134
+
135
+ def term_forks
136
+ signal_processes(forks.keys, :TERM)
137
+ end
138
+
139
+ def quit_forks
140
+ signal_processes(forks.keys, :QUIT)
141
+ end
142
+
143
+ def reap_and_replace_terminated_forks
144
+ loop do
145
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
146
+ break unless pid
147
+
148
+ replace_fork(pid, status)
149
+ end
150
+ end
151
+
152
+ def reap_terminated_forks
153
+ loop do
154
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
155
+ break unless pid
156
+
157
+ if (terminated_fork = forks.delete(pid)) && (!status.exited? || status.exitstatus > 0)
158
+ handle_claimed_jobs_by(terminated_fork, status)
159
+ end
160
+
161
+ configured_processes.delete(pid)
162
+ end
163
+ rescue SystemCallError
164
+ # All children already reaped
165
+ end
166
+
167
+ def replace_fork(pid, status)
168
+ SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
169
+ if terminated_fork = forks.delete(pid)
170
+ payload[:fork] = terminated_fork
171
+ handle_claimed_jobs_by(terminated_fork, status)
172
+
173
+ start_process(configured_processes.delete(pid))
174
+ end
175
+ end
176
+ end
177
+
178
+ def handle_claimed_jobs_by(terminated_fork, status)
179
+ if registered_process = process.supervisees.find_by(name: terminated_fork.name)
180
+ error = Processes::ProcessExitError.new(status)
181
+ registered_process.fail_all_claimed_executions_with(error)
182
+ end
183
+ end
184
+
185
+ def all_forks_terminated?
186
+ forks.empty?
187
+ end
77
188
  end
78
189
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.6.1"
2
+ VERSION = "0.7.1"
3
3
  end
@@ -2,6 +2,11 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Worker < Processes::Poller
5
+ include LifecycleHooks
6
+
7
+ after_boot :run_start_hooks
8
+ before_shutdown :run_stop_hooks
9
+
5
10
  attr_accessor :queues, :pool
6
11
 
7
12
  def initialize(**options)
data/lib/solid_queue.rb CHANGED
@@ -43,6 +43,16 @@ module SolidQueue
43
43
  mattr_accessor :clear_finished_jobs_after, default: 1.day
44
44
  mattr_accessor :default_concurrency_control_period, default: 3.minutes
45
45
 
46
+ delegate :on_start, :on_stop, to: Supervisor
47
+
48
+ def on_worker_start(...)
49
+ Worker.on_start(...)
50
+ end
51
+
52
+ def on_worker_stop(...)
53
+ Worker.on_stop(...)
54
+ end
55
+
46
56
  def supervisor?
47
57
  supervisor
48
58
  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.6.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rosa Gutierrez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-30 00:00:00.000000000 Z
11
+ date: 2024-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 1.11.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: thor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.3.1
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.3.1
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: debug
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -188,6 +202,7 @@ files:
188
202
  - MIT-LICENSE
189
203
  - README.md
190
204
  - Rakefile
205
+ - UPGRADING.md
191
206
  - app/jobs/solid_queue/recurring_job.rb
192
207
  - app/models/solid_queue/blocked_execution.rb
193
208
  - app/models/solid_queue/claimed_execution.rb
@@ -228,14 +243,17 @@ files:
228
243
  - lib/generators/solid_queue/install/USAGE
229
244
  - lib/generators/solid_queue/install/install_generator.rb
230
245
  - lib/generators/solid_queue/install/templates/config.yml
246
+ - lib/generators/solid_queue/install/templates/jobs
231
247
  - lib/puma/plugin/solid_queue.rb
232
248
  - lib/solid_queue.rb
233
249
  - lib/solid_queue/app_executor.rb
250
+ - lib/solid_queue/cli.rb
234
251
  - lib/solid_queue/configuration.rb
235
252
  - lib/solid_queue/dispatcher.rb
236
253
  - lib/solid_queue/dispatcher/concurrency_maintenance.rb
237
254
  - lib/solid_queue/dispatcher/recurring_schedule.rb
238
255
  - lib/solid_queue/engine.rb
256
+ - lib/solid_queue/lifecycle_hooks.rb
239
257
  - lib/solid_queue/log_subscriber.rb
240
258
  - lib/solid_queue/pool.rb
241
259
  - lib/solid_queue/processes/base.rb
@@ -250,8 +268,6 @@ files:
250
268
  - lib/solid_queue/processes/runnable.rb
251
269
  - lib/solid_queue/processes/supervised.rb
252
270
  - lib/solid_queue/supervisor.rb
253
- - lib/solid_queue/supervisor/async_supervisor.rb
254
- - lib/solid_queue/supervisor/fork_supervisor.rb
255
271
  - lib/solid_queue/supervisor/maintenance.rb
256
272
  - lib/solid_queue/supervisor/pidfile.rb
257
273
  - lib/solid_queue/supervisor/pidfiled.rb
@@ -267,8 +283,9 @@ metadata:
267
283
  homepage_uri: https://github.com/rails/solid_queue
268
284
  source_code_uri: https://github.com/rails/solid_queue
269
285
  post_install_message: |
270
- Upgrading to Solid Queue 0.4.x? There are some breaking changes about how Solid Queue is started. Check
271
- https://github.com/rails/solid_queue/blob/main/UPGRADING.md for upgrade instructions.
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,
287
+ configuration and new migrations. Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
288
+ for upgrade instructions.
272
289
  rdoc_options: []
273
290
  require_paths:
274
291
  - lib
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueue
4
- class Supervisor::AsyncSupervisor < Supervisor
5
- def initialize(*)
6
- super
7
- @threads = Concurrent::Map.new
8
- end
9
-
10
- def kind
11
- "Supervisor(async)"
12
- end
13
-
14
- def stop
15
- super
16
- stop_threads
17
- threads.clear
18
-
19
- shutdown
20
- end
21
-
22
- private
23
- attr_reader :threads
24
-
25
- def start_process(configured_process)
26
- process_instance = configured_process.instantiate.tap do |instance|
27
- instance.supervised_by process
28
- end
29
-
30
- process_instance.start
31
-
32
- threads[process_instance.name] = process_instance
33
- end
34
-
35
- def stop_threads
36
- stop_threads = threads.values.map do |thr|
37
- Thread.new { thr.stop }
38
- end
39
-
40
- stop_threads.each { |thr| thr.join(SolidQueue.shutdown_timeout) }
41
- end
42
-
43
- def all_threads_terminated?
44
- threads.values.none?(&:alive?)
45
- end
46
- end
47
- end
@@ -1,126 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueue
4
- class Supervisor::ForkSupervisor < Supervisor
5
- include Signals, Pidfiled
6
-
7
- def initialize(*)
8
- super
9
-
10
- @forks = {}
11
- @configured_processes = {}
12
- end
13
-
14
- def kind
15
- "Supervisor(fork)"
16
- end
17
-
18
- private
19
- attr_reader :forks, :configured_processes
20
-
21
- def supervise
22
- loop do
23
- break if stopped?
24
-
25
- procline "supervising #{forks.keys.join(", ")}"
26
- process_signal_queue
27
-
28
- unless stopped?
29
- reap_and_replace_terminated_forks
30
- interruptible_sleep(1.second)
31
- end
32
- end
33
- ensure
34
- shutdown
35
- end
36
-
37
- def start_process(configured_process)
38
- process_instance = configured_process.instantiate.tap do |instance|
39
- instance.supervised_by process
40
- instance.mode = :fork
41
- end
42
-
43
- pid = fork do
44
- process_instance.start
45
- end
46
-
47
- configured_processes[pid] = configured_process
48
- forks[pid] = process_instance
49
- end
50
-
51
- def terminate_gracefully
52
- SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do |payload|
53
- term_forks
54
-
55
- Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
56
- reap_terminated_forks
57
- end
58
-
59
- unless all_forks_terminated?
60
- payload[:shutdown_timeout_exceeded] = true
61
- terminate_immediately
62
- end
63
- end
64
- end
65
-
66
- def terminate_immediately
67
- SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do
68
- quit_forks
69
- end
70
- end
71
-
72
- def term_forks
73
- signal_processes(forks.keys, :TERM)
74
- end
75
-
76
- def quit_forks
77
- signal_processes(forks.keys, :QUIT)
78
- end
79
-
80
- def reap_and_replace_terminated_forks
81
- loop do
82
- pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
83
- break unless pid
84
-
85
- replace_fork(pid, status)
86
- end
87
- end
88
-
89
- def reap_terminated_forks
90
- loop do
91
- pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
92
- break unless pid
93
-
94
- if (terminated_fork = forks.delete(pid)) && !status.exited? || status.exitstatus > 0
95
- handle_claimed_jobs_by(terminated_fork, status)
96
- end
97
-
98
- configured_processes.delete(pid)
99
- end
100
- rescue SystemCallError
101
- # All children already reaped
102
- end
103
-
104
- def replace_fork(pid, status)
105
- SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
106
- if terminated_fork = forks.delete(pid)
107
- payload[:fork] = terminated_fork
108
- handle_claimed_jobs_by(terminated_fork, status)
109
-
110
- start_process(configured_processes.delete(pid))
111
- end
112
- end
113
- end
114
-
115
- def handle_claimed_jobs_by(terminated_fork, status)
116
- if registered_process = process.supervisees.find_by(name: terminated_fork.name)
117
- error = Processes::ProcessExitError.new(status)
118
- registered_process.fail_all_claimed_executions_with(error)
119
- end
120
- end
121
-
122
- def all_forks_terminated?
123
- forks.empty?
124
- end
125
- end
126
- end