solid_queue 0.6.1 → 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: c2734f6cbcc795345207dc0cf221bd2744f2f98449da8b0edfc8175f3bbd7869
4
- data.tar.gz: c4b4c2eca7dcb93e86a2f69d220ba1b9f8817c1d52a7cd1ff137ed5ec84e50f8
3
+ metadata.gz: 23836c755cda6dd9bec594148ad92ea19740da4125e9115d17c44bd2543d752c
4
+ data.tar.gz: 40d91182c13f155a86879f04abcf4232a50f3502f4038b5e9e07a3c15da8def2
5
5
  SHA512:
6
- metadata.gz: 9d09b58e43c4bc19ad2ab89d10072b36b53a449e9602f5c5a8fc7b637e91d8472eb70f417148657e6b146e5cd1034025afdbe6d39fa2b9fc82ce6bba3997e57f
7
- data.tar.gz: 27168fd2216fbca4e9b19677a7885b23663012d8aed7bf54c25be8335bd528a733b55790e662a30815b9ba24d824390372eff24ed0b9f72947b6ff4feeec17c5
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.
@@ -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
@@ -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)
@@ -2,16 +2,15 @@
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
12
  if configuration.configured_processes.any?
13
- klass = mode == :fork ? ForkSupervisor : AsyncSupervisor
14
- klass.new(configuration).tap(&:start)
13
+ new(configuration).tap(&:start)
15
14
  else
16
15
  abort "No workers or processed configured. Exiting..."
17
16
  end
@@ -20,6 +19,9 @@ module SolidQueue
20
19
 
21
20
  def initialize(configuration)
22
21
  @configuration = configuration
22
+ @forks = {}
23
+ @configured_processes = {}
24
+
23
25
  super
24
26
  end
25
27
 
@@ -37,7 +39,7 @@ module SolidQueue
37
39
  end
38
40
 
39
41
  private
40
- attr_reader :configuration
42
+ attr_reader :configuration, :forks, :configured_processes
41
43
 
42
44
  def boot
43
45
  SolidQueue.instrument(:start_process, process: self) do
@@ -52,15 +54,63 @@ module SolidQueue
52
54
  configuration.configured_processes.each { |configured_process| start_process(configured_process) }
53
55
  end
54
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
+
55
87
  def stopped?
56
88
  @stopped
57
89
  end
58
90
 
59
- def supervise
91
+ def set_procline
92
+ procline "supervising #{supervised_processes.join(", ")}"
60
93
  end
61
94
 
62
- def start_process(configured_process)
63
- 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
64
114
  end
65
115
 
66
116
  def shutdown
@@ -74,5 +124,63 @@ module SolidQueue
74
124
  def sync_std_streams
75
125
  STDOUT.sync = STDERR.sync = true
76
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
77
185
  end
78
186
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.6.1"
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.1
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-30 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
@@ -250,8 +267,6 @@ files:
250
267
  - lib/solid_queue/processes/runnable.rb
251
268
  - lib/solid_queue/processes/supervised.rb
252
269
  - lib/solid_queue/supervisor.rb
253
- - lib/solid_queue/supervisor/async_supervisor.rb
254
- - lib/solid_queue/supervisor/fork_supervisor.rb
255
270
  - lib/solid_queue/supervisor/maintenance.rb
256
271
  - lib/solid_queue/supervisor/pidfile.rb
257
272
  - lib/solid_queue/supervisor/pidfiled.rb
@@ -267,8 +282,9 @@ metadata:
267
282
  homepage_uri: https://github.com/rails/solid_queue
268
283
  source_code_uri: https://github.com/rails/solid_queue
269
284
  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.
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.
272
288
  rdoc_options: []
273
289
  require_paths:
274
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,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