solid_queue 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -0
- data/app/models/solid_queue/record/distinct_values.rb +12 -10
- data/app/models/solid_queue/record.rb +10 -0
- data/lib/generators/solid_queue/update/update_generator.rb +19 -0
- data/lib/solid_queue/cli.rb +4 -0
- data/lib/solid_queue/configuration.rb +9 -1
- data/lib/solid_queue/engine.rb +4 -0
- data/lib/solid_queue/fork_supervisor.rb +23 -3
- data/lib/solid_queue/log_subscriber.rb +5 -0
- data/lib/solid_queue/processes/runnable.rb +86 -1
- data/lib/solid_queue/tasks.rb +5 -0
- data/lib/solid_queue/timer.rb +3 -4
- data/lib/solid_queue/version.rb +5 -1
- data/lib/solid_queue.rb +5 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82eb2e9041fa69b3e7f3bd4a3d62c46b7826100a708b2237750f0f9e4fdee9c5
|
|
4
|
+
data.tar.gz: 14000eeba6030e0248273d4cc158e17f25f78268d5e2c62c5e74f9a959f8d94f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9bb7b225b9a3b1b13e1f2edfa04927cb2f3954d0a0b226ca79e7fc69dff6675c002ac5e6dd13fd0ff9794d2f0a660487a70cb078868034e7a048378b14911a4f
|
|
7
|
+
data.tar.gz: b30ef8f4992fc7a3d858294b3ae9b1c4459f813d9ec3bb3ff4a4fde97ad114469d76d913bb19912117de3d081e6c8d159798e5bcdd0790858ea3af6f4faf7cc6
|
data/README.md
CHANGED
|
@@ -214,6 +214,8 @@ bin/jobs -c config/calendar.yml
|
|
|
214
214
|
|
|
215
215
|
You can also skip the scheduler process by setting the environment variable `SOLID_QUEUE_SKIP_RECURRING=true`. This is useful for environments like staging, review apps, or development where you don't want any recurring jobs to run. This is equivalent to using the `--skip-recurring` option with `bin/jobs`.
|
|
216
216
|
|
|
217
|
+
To run **only** the scheduler (no workers or dispatchers)—for example to isolate recurring tasks on a dedicated process—set `SOLID_QUEUE_ONLY_RECURRING=true` or use the `--only-recurring` option with `bin/jobs`.
|
|
218
|
+
|
|
217
219
|
This is what this configuration looks like:
|
|
218
220
|
|
|
219
221
|
```yml
|
|
@@ -404,6 +406,7 @@ There are several settings that control how Solid Queue works that you can set a
|
|
|
404
406
|
- `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 only need to set this to `false` if your database doesn't support it. For MySQL, that'd be versions < 8; for MariaDB, versions < 10.6; and for PostgreSQL, versions < 9.5. If you use SQLite, this has no effect, as writes are sequential.
|
|
405
407
|
- `process_heartbeat_interval`: the heartbeat interval that all processes will follow—defaults to 60 seconds.
|
|
406
408
|
- `process_alive_threshold`: how long to wait until a process is considered dead after its last heartbeat—defaults to 5 minutes.
|
|
409
|
+
- `fork_boot_timeout`: how long a forked process can take to finish booting before the supervisor replaces it—defaults to 5 minutes. It only applies in the default `fork` mode.
|
|
407
410
|
- `shutdown_timeout`: time the supervisor will wait since it sent the `TERM` signal to its supervised processes before sending a `QUIT` version to them requesting immediate termination—defaults to 5 seconds.
|
|
408
411
|
- `silence_polling`: whether to silence Active Record logs emitted when polling for both workers and dispatchers—defaults to `true`.
|
|
409
412
|
- `supervisor_pidfile`: path to a pidfile that the supervisor will create when booting to prevent running more than one supervisor in the same host, or in case you want to use it for a health check. It's `nil` by default.
|
|
@@ -709,6 +712,8 @@ bin/jobs --recurring_schedule_file=config/schedule.yml
|
|
|
709
712
|
|
|
710
713
|
You can completely disable recurring tasks by setting the environment variable `SOLID_QUEUE_SKIP_RECURRING=true` or by using the `--skip-recurring` option with `bin/jobs`.
|
|
711
714
|
|
|
715
|
+
To run only the scheduler (no workers or dispatchers), set `SOLID_QUEUE_ONLY_RECURRING=true` or use `--only-recurring` with `bin/jobs`.
|
|
716
|
+
|
|
712
717
|
The configuration itself looks like this:
|
|
713
718
|
|
|
714
719
|
```yml
|
|
@@ -19,23 +19,25 @@ module SolidQueue
|
|
|
19
19
|
|
|
20
20
|
private
|
|
21
21
|
def loose_index_scan_emulation_needed?
|
|
22
|
-
connection.adapter_name == "PostgreSQL"
|
|
22
|
+
connection_pool.with_connection { |connection| connection.adapter_name == "PostgreSQL" }
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# Emulates a loose index scan, honoring the current scope (e.g. LIKE prefixes)
|
|
26
26
|
# by building the anchor and the recursive step as scoped relations, whose
|
|
27
27
|
# #to_sql inlines any bind parameters so they can be embedded in the raw CTE.
|
|
28
28
|
def loose_distinct_via_recursive_cte(column)
|
|
29
|
-
|
|
29
|
+
connection_pool.with_connection do |connection|
|
|
30
|
+
col = connection.quote_column_name(column)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
connection.select_values(<<~SQL.squish)
|
|
33
|
+
WITH RECURSIVE t AS (
|
|
34
|
+
(#{next_distinct_value(col, "#{col} IS NOT NULL")})
|
|
35
|
+
UNION ALL
|
|
36
|
+
SELECT (#{next_distinct_value(col, "#{col} > t.#{col}")}) FROM t WHERE t.#{col} IS NOT NULL
|
|
37
|
+
)
|
|
38
|
+
SELECT #{col} FROM t WHERE #{col} IS NOT NULL
|
|
39
|
+
SQL
|
|
40
|
+
end
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# Smallest value of `col` within the current scope that matches `condition`.
|
|
@@ -24,6 +24,16 @@ module SolidQueue
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def warn_about_pending_migrations
|
|
28
|
+
SolidQueue.deprecator.warn(<<~DEPRECATION)
|
|
29
|
+
Solid Queue has pending database migrations. To get the new migration files, run:
|
|
30
|
+
rails solid_queue:update
|
|
31
|
+
And then:
|
|
32
|
+
rails db:migrate
|
|
33
|
+
These migrations will be required after version #{SolidQueue.next_major_version}.0
|
|
34
|
+
DEPRECATION
|
|
35
|
+
end
|
|
36
|
+
|
|
27
37
|
# Pass index hints to the query optimizer using SQL comment hints.
|
|
28
38
|
# Uses MySQL 8 optimizer hint query comments, which SQLite and
|
|
29
39
|
# PostgreSQL ignore.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/active_record"
|
|
4
|
+
|
|
5
|
+
class SolidQueue::UpdateGenerator < Rails::Generators::Base
|
|
6
|
+
include ActiveRecord::Generators::Migration
|
|
7
|
+
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
class_option :database, type: :string, aliases: %i[ --db ], default: "queue",
|
|
11
|
+
desc: "The database that Solid Queue uses. Defaults to `queue`"
|
|
12
|
+
|
|
13
|
+
def copy_new_migrations
|
|
14
|
+
Dir.glob(File.join(self.class.source_root, "db", "*.rb")).each do |migration_file|
|
|
15
|
+
name = File.basename(migration_file)
|
|
16
|
+
migration_template File.join("db", name), File.join(db_migrate_path, name), skip: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/solid_queue/cli.rb
CHANGED
|
@@ -20,6 +20,10 @@ module SolidQueue
|
|
|
20
20
|
desc: "Whether to skip recurring tasks scheduling",
|
|
21
21
|
banner: "SOLID_QUEUE_SKIP_RECURRING"
|
|
22
22
|
|
|
23
|
+
class_option :only_recurring, type: :boolean,
|
|
24
|
+
desc: "Whether to run only the scheduler process for recurring tasks",
|
|
25
|
+
banner: "SOLID_QUEUE_ONLY_RECURRING"
|
|
26
|
+
|
|
23
27
|
def self.exit_on_failure?
|
|
24
28
|
true
|
|
25
29
|
end
|
|
@@ -43,7 +43,10 @@ module SolidQueue
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def configured_processes
|
|
46
|
-
if only_work?
|
|
46
|
+
if only_work?
|
|
47
|
+
workers
|
|
48
|
+
elsif only_recurring?
|
|
49
|
+
schedulers
|
|
47
50
|
else
|
|
48
51
|
dispatchers + workers + schedulers
|
|
49
52
|
end
|
|
@@ -126,6 +129,7 @@ module SolidQueue
|
|
|
126
129
|
recurring_schedule_file: Rails.root.join(ENV["SOLID_QUEUE_RECURRING_SCHEDULE"] || DEFAULT_RECURRING_SCHEDULE_FILE_PATH),
|
|
127
130
|
only_work: false,
|
|
128
131
|
only_dispatch: false,
|
|
132
|
+
only_recurring: ActiveModel::Type::Boolean.new.cast(ENV["SOLID_QUEUE_ONLY_RECURRING"]),
|
|
129
133
|
skip_recurring: ActiveModel::Type::Boolean.new.cast(ENV["SOLID_QUEUE_SKIP_RECURRING"])
|
|
130
134
|
}
|
|
131
135
|
end
|
|
@@ -142,6 +146,10 @@ module SolidQueue
|
|
|
142
146
|
options[:only_dispatch]
|
|
143
147
|
end
|
|
144
148
|
|
|
149
|
+
def only_recurring?
|
|
150
|
+
options[:only_recurring]
|
|
151
|
+
end
|
|
152
|
+
|
|
145
153
|
def skip_recurring_tasks?
|
|
146
154
|
options[:skip_recurring] || only_work?
|
|
147
155
|
end
|
data/lib/solid_queue/engine.rb
CHANGED
|
@@ -31,6 +31,21 @@ module SolidQueue
|
|
|
31
31
|
|
|
32
32
|
replace_fork(pid, status)
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
check_boot_timeouts
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def check_boot_timeouts
|
|
39
|
+
process_instances.each do |pid, instance|
|
|
40
|
+
terminate_unready_process(pid) if instance.boot_timed_out?
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def terminate_unready_process(pid)
|
|
45
|
+
SolidQueue.instrument(:fork_boot_timeout, process: process_instances[pid], pid: pid) do
|
|
46
|
+
# A child stuck in boot cannot reach its run loop to stop gracefully
|
|
47
|
+
signal_process(pid, :KILL)
|
|
48
|
+
end
|
|
34
49
|
end
|
|
35
50
|
|
|
36
51
|
def reap_terminated_forks
|
|
@@ -38,9 +53,13 @@ module SolidQueue
|
|
|
38
53
|
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
39
54
|
break unless pid
|
|
40
55
|
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
if terminated_fork = process_instances.delete(pid)
|
|
57
|
+
terminated_fork.mark_as_reaped
|
|
58
|
+
|
|
59
|
+
if !status.exited? || status.exitstatus.to_i > 0
|
|
60
|
+
error = Processes::ProcessExitError.new(status)
|
|
61
|
+
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
62
|
+
end
|
|
44
63
|
end
|
|
45
64
|
|
|
46
65
|
configured_processes.delete(pid)
|
|
@@ -52,6 +71,7 @@ module SolidQueue
|
|
|
52
71
|
def replace_fork(pid, status)
|
|
53
72
|
SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
|
|
54
73
|
if terminated_fork = process_instances.delete(pid)
|
|
74
|
+
terminated_fork.mark_as_reaped
|
|
55
75
|
payload[:fork] = terminated_fork
|
|
56
76
|
error = Processes::ProcessExitError.new(status)
|
|
57
77
|
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
@@ -161,6 +161,11 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
+
def fork_boot_timeout(event)
|
|
165
|
+
process = event.payload[:process]
|
|
166
|
+
warn formatted_event(event, action: "Terminate #{process.kind} that failed to boot in time", **event.payload.slice(:pid).merge(hostname: process.hostname, name: process.name))
|
|
167
|
+
end
|
|
168
|
+
|
|
164
169
|
private
|
|
165
170
|
def formatted_event(event, action:, **attributes)
|
|
166
171
|
"SolidQueue-#{SolidQueue::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
|
|
@@ -31,6 +31,14 @@ module SolidQueue::Processes
|
|
|
31
31
|
!running_async? || @thread&.alive?
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def boot_timed_out?
|
|
35
|
+
@boot_guard.timed_out?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def mark_as_reaped
|
|
39
|
+
@boot_guard.close
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
private
|
|
35
43
|
DEFAULT_MODE = :async
|
|
36
44
|
|
|
@@ -41,11 +49,14 @@ module SolidQueue::Processes
|
|
|
41
49
|
def run_in_mode(&block)
|
|
42
50
|
case
|
|
43
51
|
when running_as_fork?
|
|
44
|
-
|
|
52
|
+
@boot_guard = BootGuards::ForkGuard.new
|
|
53
|
+
fork(&block).tap { @boot_guard.start }
|
|
45
54
|
when running_async?
|
|
55
|
+
@boot_guard = BootGuards::NullGuard.new
|
|
46
56
|
@thread = create_thread(&block)
|
|
47
57
|
@thread.object_id
|
|
48
58
|
else
|
|
59
|
+
@boot_guard = BootGuards::NullGuard.new
|
|
49
60
|
block.call
|
|
50
61
|
end
|
|
51
62
|
end
|
|
@@ -59,6 +70,8 @@ module SolidQueue::Processes
|
|
|
59
70
|
end
|
|
60
71
|
end
|
|
61
72
|
end
|
|
73
|
+
|
|
74
|
+
@boot_guard.complete
|
|
62
75
|
end
|
|
63
76
|
|
|
64
77
|
def shutting_down?
|
|
@@ -95,4 +108,76 @@ module SolidQueue::Processes
|
|
|
95
108
|
mode.fork?
|
|
96
109
|
end
|
|
97
110
|
end
|
|
111
|
+
|
|
112
|
+
module BootGuards
|
|
113
|
+
# Tracks a process that shares memory with its supervisor, whose boot time
|
|
114
|
+
# doesn't need monitoring.
|
|
115
|
+
class NullGuard
|
|
116
|
+
def complete
|
|
117
|
+
@completed = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def start
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def completed?
|
|
124
|
+
@completed
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def timed_out?
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def close
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Tracks a forked process from the moment it's started until its boot
|
|
136
|
+
# callbacks finish, over a pipe that survives forking: the forked process
|
|
137
|
+
# writes to it when it's done booting, and its supervisor reads from it to
|
|
138
|
+
# decide whether the process is taking too long to boot and needs replacing.
|
|
139
|
+
class ForkGuard
|
|
140
|
+
def initialize
|
|
141
|
+
@reader, @writer = IO.pipe
|
|
142
|
+
@created_at = SolidQueue::Timer.monotonic_time_now
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Runs in the forked process when it has finished booting
|
|
146
|
+
def complete
|
|
147
|
+
reader.close
|
|
148
|
+
writer.write(".")
|
|
149
|
+
rescue Errno::EPIPE
|
|
150
|
+
# The supervisor stopped waiting while this process finished booting
|
|
151
|
+
ensure
|
|
152
|
+
writer.close
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Runs in the parent process right after forking
|
|
156
|
+
def start
|
|
157
|
+
writer.close
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A byte means boot completed; EOF means the process exited before
|
|
161
|
+
# finishing its boot, and will be replaced when it's reaped
|
|
162
|
+
def completed?
|
|
163
|
+
@completed ||= begin
|
|
164
|
+
completed = reader.read_nonblock(1, exception: false) != :wait_readable
|
|
165
|
+
reader.close if completed
|
|
166
|
+
completed
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def timed_out?
|
|
171
|
+
!completed? && SolidQueue::Timer.monotonic_time_now - created_at >= SolidQueue.fork_boot_timeout
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def close
|
|
175
|
+
reader.close unless reader.closed?
|
|
176
|
+
writer.close unless writer.closed?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
attr_reader :reader, :writer, :created_at
|
|
181
|
+
end
|
|
182
|
+
end
|
|
98
183
|
end
|
data/lib/solid_queue/tasks.rb
CHANGED
|
@@ -4,6 +4,11 @@ namespace :solid_queue do
|
|
|
4
4
|
Rails::Command.invoke :generate, [ "solid_queue:install" ]
|
|
5
5
|
end
|
|
6
6
|
|
|
7
|
+
desc "Copy any new Solid Queue migrations to the application"
|
|
8
|
+
task :update do
|
|
9
|
+
Rails::Command.invoke :generate, [ "solid_queue:update" ]
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
desc "start solid_queue supervisor to dispatch and process jobs"
|
|
8
13
|
task start: :environment do
|
|
9
14
|
SolidQueue::Supervisor.start
|
data/lib/solid_queue/timer.rb
CHANGED
data/lib/solid_queue/version.rb
CHANGED
data/lib/solid_queue.rb
CHANGED
|
@@ -29,6 +29,7 @@ module SolidQueue
|
|
|
29
29
|
|
|
30
30
|
mattr_accessor :process_heartbeat_interval, default: 60.seconds
|
|
31
31
|
mattr_accessor :process_alive_threshold, default: 5.minutes
|
|
32
|
+
mattr_accessor :fork_boot_timeout, default: 5.minutes
|
|
32
33
|
|
|
33
34
|
mattr_accessor :shutdown_timeout, default: 5.seconds
|
|
34
35
|
|
|
@@ -86,6 +87,10 @@ module SolidQueue
|
|
|
86
87
|
preserve_finished_jobs
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
def deprecator
|
|
91
|
+
@deprecator ||= ActiveSupport::Deprecation.new(next_major_version, "SolidQueue")
|
|
92
|
+
end
|
|
93
|
+
|
|
89
94
|
def instrument(channel, **options, &block)
|
|
90
95
|
ActiveSupport::Notifications.instrument("#{channel}.solid_queue", **options, &block)
|
|
91
96
|
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: 1.5.
|
|
4
|
+
version: 1.5.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: 2026-07-
|
|
11
|
+
date: 2026-07-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -309,6 +309,7 @@ files:
|
|
|
309
309
|
- lib/generators/solid_queue/install/templates/config/queue.yml
|
|
310
310
|
- lib/generators/solid_queue/install/templates/config/recurring.yml
|
|
311
311
|
- lib/generators/solid_queue/install/templates/db/queue_schema.rb
|
|
312
|
+
- lib/generators/solid_queue/update/update_generator.rb
|
|
312
313
|
- lib/puma/plugin/solid_queue.rb
|
|
313
314
|
- lib/solid_queue.rb
|
|
314
315
|
- lib/solid_queue/app_executor.rb
|