solid_queue 0.6.0 → 0.7.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: e62ec09bbed1f2bcb96da96865c3516adbc254ad7cea234d3b6573ea001c758e
4
- data.tar.gz: d78e7543194f3f6470bdb80c52ffca78c520a602d93423185c02ba82475bbda6
3
+ metadata.gz: 23836c755cda6dd9bec594148ad92ea19740da4125e9115d17c44bd2543d752c
4
+ data.tar.gz: 40d91182c13f155a86879f04abcf4232a50f3502f4038b5e9e07a3c15da8def2
5
5
  SHA512:
6
- metadata.gz: e7cdceced9162911efdd0f2020042e45f7c73c74c506cf1d4958211b2cc34f02e96553d9910b3d3cca925f00e10960837be4872a33c3e44388e0b6c37cbcec31
7
- data.tar.gz: d9a17282687ef9feaa4b357124411e9bd088bebb8c0ce6fd67ccf887e4d9cf20a431882e84b40f68ff5ca05a008258bf4d64b6ab86b675cb581be65154d1e39e
6
+ metadata.gz: f8ac683f1a0e635762d0d93b9b74f712c5fc5465381ea51230448dc2f176de67aafb0ee7031ae9e9cfee73edcee7bd70352e78aefd49ae25ea71a7485af95aff
7
+ data.tar.gz: b02eac304a30fb8c7e9e2627446a31bb56d22740e863b1d2749838429416f886da78d19d2000b533e3672576387ee075b8efb3c330f6177b5cd76a27955cd433
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
 
@@ -305,18 +305,6 @@ plugin :solid_queue
305
305
  ```
306
306
  to your `puma.rb` configuration.
307
307
 
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
308
 
321
309
  ## Jobs and transactional integrity
322
310
  :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.
@@ -29,8 +29,9 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
29
29
  def release_all
30
30
  SolidQueue.instrument(:release_many_claimed) do |payload|
31
31
  includes(:job).tap do |executions|
32
- payload[:size] = executions.size
33
32
  executions.each(&:release)
33
+
34
+ payload[:size] = executions.size
34
35
  end
35
36
  end
36
37
  end
@@ -38,11 +39,11 @@ class SolidQueue::ClaimedExecution < SolidQueue::Execution
38
39
  def fail_all_with(error)
39
40
  SolidQueue.instrument(:fail_many_claimed) do |payload|
40
41
  includes(:job).tap do |executions|
41
- payload[:size] = executions.size
42
+ executions.each { |execution| execution.failed_with(error) }
43
+
42
44
  payload[:process_ids] = executions.map(&:process_id).uniq
43
45
  payload[:job_ids] = executions.map(&:job_id).uniq
44
-
45
- executions.each { |execution| execution.failed_with(error) }
46
+ payload[:size] = executions.size
46
47
  end
47
48
  end
48
49
  end
@@ -8,7 +8,7 @@ module SolidQueue
8
8
  included do
9
9
  has_many :claimed_executions
10
10
 
11
- after_destroy -> { claimed_executions.release_all }, if: :claims_executions?
11
+ after_destroy :release_all_claimed_executions
12
12
  end
13
13
 
14
14
  def fail_all_claimed_executions_with(error)
@@ -17,6 +17,12 @@ module SolidQueue
17
17
  end
18
18
  end
19
19
 
20
+ def release_all_claimed_executions
21
+ if claims_executions?
22
+ claimed_executions.release_all
23
+ end
24
+ end
25
+
20
26
  private
21
27
  def claims_executions?
22
28
  kind == "Worker"
@@ -1,12 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueue
4
- class ProcessPrunedError < RuntimeError
5
- def initialize(last_heartbeat_at)
6
- super("Process was found dead and pruned (last heartbeat at: #{last_heartbeat_at}")
7
- end
8
- end
9
-
10
4
  class Process
11
5
  module Prunable
12
6
  extend ActiveSupport::Concern
@@ -28,7 +22,7 @@ module SolidQueue
28
22
  end
29
23
 
30
24
  def prune
31
- error = ProcessPrunedError.new(last_heartbeat_at)
25
+ error = Processes::ProcessPrunedError.new(last_heartbeat_at)
32
26
  fail_all_claimed_executions_with(error)
33
27
 
34
28
  deregister(pruned: true)
@@ -4,7 +4,7 @@ class SolidQueue::Process < SolidQueue::Record
4
4
  include Executor, Prunable
5
5
 
6
6
  belongs_to :supervisor, class_name: "SolidQueue::Process", optional: true, inverse_of: :supervisees
7
- has_many :supervisees, class_name: "SolidQueue::Process", inverse_of: :supervisor, foreign_key: :supervisor_id, dependent: :destroy
7
+ has_many :supervisees, class_name: "SolidQueue::Process", inverse_of: :supervisor, foreign_key: :supervisor_id
8
8
 
9
9
  store :metadata, coder: JSON
10
10
 
@@ -26,9 +26,18 @@ class SolidQueue::Process < SolidQueue::Record
26
26
  def deregister(pruned: false)
27
27
  SolidQueue.instrument :deregister_process, process: self, pruned: pruned do |payload|
28
28
  destroy!
29
+
30
+ unless supervised? || pruned
31
+ supervisees.each(&:deregister)
32
+ end
29
33
  rescue Exception => error
30
34
  payload[:error] = error
31
35
  raise
32
36
  end
33
37
  end
38
+
39
+ private
40
+ def supervised?
41
+ supervisor_id.present?
42
+ end
34
43
  end
@@ -11,6 +11,6 @@ class MakeNameNotNull < ActiveRecord::Migration[7.1]
11
11
 
12
12
  def down
13
13
  remove_index :solid_queue_processes, [ :name, :supervisor_id ]
14
- change_column :solid_queue_processes, :name, :string, null: false
14
+ change_column :solid_queue_processes, :name, :string, null: true
15
15
  end
16
16
  end
@@ -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
@@ -23,8 +23,12 @@ module SolidQueue
23
23
  recurring_tasks: []
24
24
  }
25
25
 
26
- def initialize(mode: :fork, load_from: nil)
27
- @mode = mode.to_s.inquiry
26
+ DEFAULT_CONFIG = {
27
+ workers: [ WORKER_DEFAULTS ],
28
+ dispatchers: [ DISPATCHER_DEFAULTS ]
29
+ }
30
+
31
+ def initialize(load_from: nil)
28
32
  @raw_config = config_from(load_from)
29
33
  end
30
34
 
@@ -38,17 +42,13 @@ module SolidQueue
38
42
  end
39
43
 
40
44
  private
41
- attr_reader :raw_config, :mode
45
+ attr_reader :raw_config
42
46
 
43
47
  DEFAULT_CONFIG_FILE_PATH = "config/solid_queue.yml"
44
48
 
45
49
  def workers
46
50
  workers_options.flat_map do |worker_options|
47
- processes = if mode.fork?
48
- worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
49
- else
50
- WORKER_DEFAULTS[:processes]
51
- end
51
+ processes = worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
52
52
  processes.times.map { Process.new(:worker, worker_options.with_defaults(WORKER_DEFAULTS)) }
53
53
  end
54
54
  end
@@ -61,22 +61,27 @@ module SolidQueue
61
61
  end
62
62
 
63
63
  def config_from(file_or_hash, env: Rails.env)
64
- config = load_config_from(file_or_hash)
65
- config[env.to_sym] ? config[env.to_sym] : config
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
70
+ end
66
71
  end
67
72
 
68
73
  def workers_options
69
- @workers_options ||= options_from_raw_config(:workers, WORKER_DEFAULTS)
74
+ @workers_options ||= options_from_raw_config(:workers)
70
75
  .map { |options| options.dup.symbolize_keys }
71
76
  end
72
77
 
73
78
  def dispatchers_options
74
- @dispatchers_options ||= options_from_raw_config(:dispatchers, DISPATCHER_DEFAULTS)
79
+ @dispatchers_options ||= options_from_raw_config(:dispatchers)
75
80
  .map { |options| options.dup.symbolize_keys }
76
81
  end
77
82
 
78
- def options_from_raw_config(key, defaults)
79
- raw_config.empty? ? [ defaults ] : Array(raw_config[key])
83
+ def options_from_raw_config(key)
84
+ Array(raw_config[key])
80
85
  end
81
86
 
82
87
  def parse_recurring_tasks(tasks)
@@ -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
@@ -45,10 +45,12 @@ module SolidQueue::Processes
45
45
  end
46
46
 
47
47
  def with_polling_volume
48
- if SolidQueue.silence_polling? && ActiveRecord::Base.logger
49
- ActiveRecord::Base.logger.silence { yield }
50
- else
51
- yield
48
+ SolidQueue.instrument(:polling) do
49
+ if SolidQueue.silence_polling? && ActiveRecord::Base.logger
50
+ ActiveRecord::Base.logger.silence { yield }
51
+ else
52
+ yield
53
+ end
52
54
  end
53
55
  end
54
56
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ module Processes
5
+ class ProcessExitError < RuntimeError
6
+ def initialize(status)
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}."
14
+ end
15
+
16
+ super(message)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,9 @@
1
+ module SolidQueue
2
+ module Processes
3
+ class ProcessMissingError < RuntimeError
4
+ def initialize
5
+ super("The process that was running this job no longer exists")
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ module Processes
5
+ class ProcessPrunedError < RuntimeError
6
+ def initialize(last_heartbeat_at)
7
+ super("Process was found dead and pruned (last heartbeat at: #{last_heartbeat_at}")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,10 +1,4 @@
1
1
  module SolidQueue
2
- class ProcessMissingError < RuntimeError
3
- def initialize
4
- super("The process that was running this job no longer exists")
5
- end
6
- end
7
-
8
2
  module Supervisor::Maintenance
9
3
  extend ActiveSupport::Concern
10
4
 
@@ -35,7 +29,7 @@ module SolidQueue
35
29
 
36
30
  def fail_orphaned_executions
37
31
  wrap_in_app_executor do
38
- SolidQueue::ClaimedExecution.orphaned.fail_all_with(ProcessMissingError.new)
32
+ ClaimedExecution.orphaned.fail_all_with(Processes::ProcessMissingError.new)
39
33
  end
40
34
  end
41
35
  end
@@ -2,20 +2,26 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Supervisor < Processes::Base
5
- include Maintenance
5
+ include Maintenance, Signals, Pidfiled
6
6
 
7
7
  class << self
8
- def start(mode: :fork, load_configuration_from: nil)
8
+ def start(load_configuration_from: nil)
9
9
  SolidQueue.supervisor = true
10
- configuration = Configuration.new(mode: mode, load_from: load_configuration_from)
10
+ configuration = Configuration.new(load_from: load_configuration_from)
11
11
 
12
- klass = mode == :fork ? ForkSupervisor : AsyncSupervisor
13
- klass.new(configuration).tap(&:start)
12
+ if configuration.configured_processes.any?
13
+ new(configuration).tap(&:start)
14
+ else
15
+ abort "No workers or processed configured. Exiting..."
16
+ end
14
17
  end
15
18
  end
16
19
 
17
20
  def initialize(configuration)
18
21
  @configuration = configuration
22
+ @forks = {}
23
+ @configured_processes = {}
24
+
19
25
  super
20
26
  end
21
27
 
@@ -33,7 +39,7 @@ module SolidQueue
33
39
  end
34
40
 
35
41
  private
36
- attr_reader :configuration
42
+ attr_reader :configuration, :forks, :configured_processes
37
43
 
38
44
  def boot
39
45
  SolidQueue.instrument(:start_process, process: self) do
@@ -48,15 +54,63 @@ module SolidQueue
48
54
  configuration.configured_processes.each { |configured_process| start_process(configured_process) }
49
55
  end
50
56
 
57
+ def supervise
58
+ loop do
59
+ break if stopped?
60
+
61
+ set_procline
62
+ process_signal_queue
63
+
64
+ unless stopped?
65
+ reap_and_replace_terminated_forks
66
+ interruptible_sleep(1.second)
67
+ end
68
+ end
69
+ ensure
70
+ shutdown
71
+ end
72
+
73
+ def start_process(configured_process)
74
+ process_instance = configured_process.instantiate.tap do |instance|
75
+ instance.supervised_by process
76
+ instance.mode = :fork
77
+ end
78
+
79
+ pid = fork do
80
+ process_instance.start
81
+ end
82
+
83
+ configured_processes[pid] = configured_process
84
+ forks[pid] = process_instance
85
+ end
86
+
51
87
  def stopped?
52
88
  @stopped
53
89
  end
54
90
 
55
- def supervise
91
+ def set_procline
92
+ procline "supervising #{supervised_processes.join(", ")}"
56
93
  end
57
94
 
58
- def start_process(configured_process)
59
- raise NotImplementedError
95
+ def terminate_gracefully
96
+ SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do |payload|
97
+ term_forks
98
+
99
+ Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
100
+ reap_terminated_forks
101
+ end
102
+
103
+ unless all_forks_terminated?
104
+ payload[:shutdown_timeout_exceeded] = true
105
+ terminate_immediately
106
+ end
107
+ end
108
+ end
109
+
110
+ def terminate_immediately
111
+ SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do
112
+ quit_forks
113
+ end
60
114
  end
61
115
 
62
116
  def shutdown
@@ -70,5 +124,63 @@ module SolidQueue
70
124
  def sync_std_streams
71
125
  STDOUT.sync = STDERR.sync = true
72
126
  end
127
+
128
+ def supervised_processes
129
+ forks.keys
130
+ end
131
+
132
+ def term_forks
133
+ signal_processes(forks.keys, :TERM)
134
+ end
135
+
136
+ def quit_forks
137
+ signal_processes(forks.keys, :QUIT)
138
+ end
139
+
140
+ def reap_and_replace_terminated_forks
141
+ loop do
142
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
143
+ break unless pid
144
+
145
+ replace_fork(pid, status)
146
+ end
147
+ end
148
+
149
+ def reap_terminated_forks
150
+ loop do
151
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
152
+ break unless pid
153
+
154
+ if (terminated_fork = forks.delete(pid)) && (!status.exited? || status.exitstatus > 0)
155
+ handle_claimed_jobs_by(terminated_fork, status)
156
+ end
157
+
158
+ configured_processes.delete(pid)
159
+ end
160
+ rescue SystemCallError
161
+ # All children already reaped
162
+ end
163
+
164
+ def replace_fork(pid, status)
165
+ SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
166
+ if terminated_fork = forks.delete(pid)
167
+ payload[:fork] = terminated_fork
168
+ handle_claimed_jobs_by(terminated_fork, status)
169
+
170
+ start_process(configured_processes.delete(pid))
171
+ end
172
+ end
173
+ end
174
+
175
+ def handle_claimed_jobs_by(terminated_fork, status)
176
+ if registered_process = process.supervisees.find_by(name: terminated_fork.name)
177
+ error = Processes::ProcessExitError.new(status)
178
+ registered_process.fail_all_claimed_executions_with(error)
179
+ end
180
+ end
181
+
182
+ def all_forks_terminated?
183
+ forks.empty?
184
+ end
73
185
  end
74
186
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.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.6.0
4
+ version: 0.7.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-08-21 00:00:00.000000000 Z
11
+ date: 2024-09-02 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,9 +243,11 @@ 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
@@ -242,13 +259,14 @@ files:
242
259
  - lib/solid_queue/processes/callbacks.rb
243
260
  - lib/solid_queue/processes/interruptible.rb
244
261
  - lib/solid_queue/processes/poller.rb
262
+ - lib/solid_queue/processes/process_exit_error.rb
263
+ - lib/solid_queue/processes/process_missing_error.rb
264
+ - lib/solid_queue/processes/process_pruned_error.rb
245
265
  - lib/solid_queue/processes/procline.rb
246
266
  - lib/solid_queue/processes/registrable.rb
247
267
  - lib/solid_queue/processes/runnable.rb
248
268
  - lib/solid_queue/processes/supervised.rb
249
269
  - lib/solid_queue/supervisor.rb
250
- - lib/solid_queue/supervisor/async_supervisor.rb
251
- - lib/solid_queue/supervisor/fork_supervisor.rb
252
270
  - lib/solid_queue/supervisor/maintenance.rb
253
271
  - lib/solid_queue/supervisor/pidfile.rb
254
272
  - lib/solid_queue/supervisor/pidfiled.rb
@@ -264,8 +282,9 @@ metadata:
264
282
  homepage_uri: https://github.com/rails/solid_queue
265
283
  source_code_uri: https://github.com/rails/solid_queue
266
284
  post_install_message: |
267
- Upgrading to Solid Queue 0.4.x? There are some breaking changes about how Solid Queue is started. Check
268
- https://github.com/rails/solid_queue/blob/main/UPGRADING.md for upgrade instructions.
285
+ 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,
286
+ configuration and new migrations. Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
287
+ for upgrade instructions.
269
288
  rdoc_options: []
270
289
  require_paths:
271
290
  - 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,138 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueue
4
- class ProcessExitError < RuntimeError
5
- def initialize(status)
6
- message = case
7
- when status.exitstatus.present? then "Process pid=#{status.pid} exited with status #{status.exitstatus}"
8
- when status.signaled? then "Process pid=#{status.pid} received unhandled signal #{status.termsig}"
9
- else "Process pid=#{status.pid} exited unexpectedly"
10
- end
11
-
12
- super(message)
13
- end
14
- end
15
-
16
- class Supervisor::ForkSupervisor < Supervisor
17
- include Signals, Pidfiled
18
-
19
- def initialize(*)
20
- super
21
-
22
- @forks = {}
23
- @configured_processes = {}
24
- end
25
-
26
- def kind
27
- "Supervisor(fork)"
28
- end
29
-
30
- private
31
- attr_reader :forks, :configured_processes
32
-
33
- def supervise
34
- loop do
35
- break if stopped?
36
-
37
- procline "supervising #{forks.keys.join(", ")}"
38
- process_signal_queue
39
-
40
- unless stopped?
41
- reap_and_replace_terminated_forks
42
- interruptible_sleep(1.second)
43
- end
44
- end
45
- ensure
46
- shutdown
47
- end
48
-
49
- def start_process(configured_process)
50
- process_instance = configured_process.instantiate.tap do |instance|
51
- instance.supervised_by process
52
- instance.mode = :fork
53
- end
54
-
55
- pid = fork do
56
- process_instance.start
57
- end
58
-
59
- configured_processes[pid] = configured_process
60
- forks[pid] = process_instance
61
- end
62
-
63
- def terminate_gracefully
64
- SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do |payload|
65
- term_forks
66
-
67
- Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
68
- reap_terminated_forks
69
- end
70
-
71
- unless all_forks_terminated?
72
- payload[:shutdown_timeout_exceeded] = true
73
- terminate_immediately
74
- end
75
- end
76
- end
77
-
78
- def terminate_immediately
79
- SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do
80
- quit_forks
81
- end
82
- end
83
-
84
- def term_forks
85
- signal_processes(forks.keys, :TERM)
86
- end
87
-
88
- def quit_forks
89
- signal_processes(forks.keys, :QUIT)
90
- end
91
-
92
- def reap_and_replace_terminated_forks
93
- loop do
94
- pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
95
- break unless pid
96
-
97
- replace_fork(pid, status)
98
- end
99
- end
100
-
101
- def reap_terminated_forks
102
- loop do
103
- pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
104
- break unless pid
105
-
106
- if (terminated_fork = forks.delete(pid)) && !status.exited? || status.exitstatus > 0
107
- handle_claimed_jobs_by(terminated_fork, status)
108
- end
109
-
110
- configured_processes.delete(pid)
111
- end
112
- rescue SystemCallError
113
- # All children already reaped
114
- end
115
-
116
- def replace_fork(pid, status)
117
- SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
118
- if terminated_fork = forks.delete(pid)
119
- payload[:fork] = terminated_fork
120
- handle_claimed_jobs_by(terminated_fork, status)
121
-
122
- start_process(configured_processes.delete(pid))
123
- end
124
- end
125
- end
126
-
127
- def handle_claimed_jobs_by(terminated_fork, status)
128
- if registered_process = process.supervisees.find_by(name: terminated_fork.name)
129
- error = ProcessExitError.new(status)
130
- registered_process.fail_all_claimed_executions_with(error)
131
- end
132
- end
133
-
134
- def all_forks_terminated?
135
- forks.empty?
136
- end
137
- end
138
- end