solid_queue 1.2.1 → 1.4.0

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -32
  3. data/app/models/solid_queue/blocked_execution.rb +3 -1
  4. data/app/models/solid_queue/claimed_execution.rb +4 -2
  5. data/app/models/solid_queue/failed_execution.rb +6 -3
  6. data/app/models/solid_queue/job/concurrency_controls.rb +1 -1
  7. data/app/models/solid_queue/job/retryable.rb +10 -1
  8. data/app/models/solid_queue/job.rb +1 -0
  9. data/app/models/solid_queue/ready_execution.rb +2 -1
  10. data/app/models/solid_queue/record.rb +20 -5
  11. data/app/models/solid_queue/recurring_execution.rb +1 -1
  12. data/app/models/solid_queue/recurring_task.rb +27 -9
  13. data/app/models/solid_queue/semaphore.rb +2 -2
  14. data/lib/puma/plugin/solid_queue.rb +74 -14
  15. data/lib/solid_queue/app_executor.rb +10 -0
  16. data/lib/solid_queue/async_supervisor.rb +52 -0
  17. data/lib/solid_queue/cli.rb +5 -1
  18. data/lib/solid_queue/configuration.rb +42 -8
  19. data/lib/solid_queue/dispatcher.rb +1 -0
  20. data/lib/solid_queue/fork_supervisor.rb +68 -0
  21. data/lib/solid_queue/processes/registrable.rb +15 -9
  22. data/lib/solid_queue/processes/runnable.rb +25 -18
  23. data/lib/solid_queue/processes/thread_terminated_error.rb +11 -0
  24. data/lib/solid_queue/scheduler/recurring_schedule.rb +61 -11
  25. data/lib/solid_queue/scheduler.rb +23 -4
  26. data/lib/solid_queue/supervisor/maintenance.rb +11 -0
  27. data/lib/solid_queue/supervisor/signals.rb +2 -2
  28. data/lib/solid_queue/supervisor.rb +40 -82
  29. data/lib/solid_queue/timer.rb +3 -3
  30. data/lib/solid_queue/version.rb +1 -1
  31. data/lib/solid_queue.rb +8 -0
  32. metadata +25 -9
  33. data/Rakefile +0 -43
@@ -32,5 +32,16 @@ module SolidQueue
32
32
  ClaimedExecution.orphaned.fail_all_with(Processes::ProcessMissingError.new)
33
33
  end
34
34
  end
35
+
36
+ # When a supervised process crashes or exits we need to mark all the
37
+ # executions it had claimed as failed so that they can be retried
38
+ # by some other worker.
39
+ def release_claimed_jobs_by(terminated_process, with_error:)
40
+ wrap_in_app_executor do
41
+ if registered_process = SolidQueue::Process.find_by(name: terminated_process.name)
42
+ registered_process.fail_all_claimed_executions_with(with_error)
43
+ end
44
+ end
45
+ end
35
46
  end
36
47
  end
@@ -6,8 +6,8 @@ module SolidQueue
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- before_boot :register_signal_handlers
10
- after_shutdown :restore_default_signal_handlers
9
+ before_boot :register_signal_handlers, if: :standalone?
10
+ after_shutdown :restore_default_signal_handlers, if: :standalone?
11
11
  end
12
12
 
13
13
  private
@@ -13,17 +13,21 @@ module SolidQueue
13
13
  configuration = Configuration.new(**options)
14
14
 
15
15
  if configuration.valid?
16
- new(configuration).tap(&:start)
16
+ klass = configuration.mode.fork? ? ForkSupervisor : AsyncSupervisor
17
+ klass.new(configuration).tap(&:start)
17
18
  else
18
19
  abort configuration.errors.full_messages.join("\n") + "\nExiting..."
19
20
  end
20
21
  end
21
22
  end
22
23
 
24
+ delegate :mode, :standalone?, to: :configuration
25
+
23
26
  def initialize(configuration)
24
27
  @configuration = configuration
25
- @forks = {}
28
+
26
29
  @configured_processes = {}
30
+ @process_instances = {}
27
31
 
28
32
  super
29
33
  end
@@ -43,8 +47,12 @@ module SolidQueue
43
47
  run_stop_hooks
44
48
  end
45
49
 
50
+ def kind
51
+ "Supervisor(#{mode})"
52
+ end
53
+
46
54
  private
47
- attr_reader :configuration, :forks, :configured_processes
55
+ attr_reader :configuration, :configured_processes, :process_instances
48
56
 
49
57
  def boot
50
58
  SolidQueue.instrument(:start_process, process: self) do
@@ -62,11 +70,13 @@ module SolidQueue
62
70
  loop do
63
71
  break if stopped?
64
72
 
65
- set_procline
66
- process_signal_queue
73
+ if standalone?
74
+ set_procline
75
+ process_signal_queue
76
+ end
67
77
 
68
78
  unless stopped?
69
- reap_and_replace_terminated_forks
79
+ check_and_replace_terminated_processes
70
80
  interruptible_sleep(1.second)
71
81
  end
72
82
  end
@@ -77,30 +87,23 @@ module SolidQueue
77
87
  def start_process(configured_process)
78
88
  process_instance = configured_process.instantiate.tap do |instance|
79
89
  instance.supervised_by process
80
- instance.mode = :fork
90
+ instance.mode = mode
81
91
  end
82
92
 
83
- pid = fork do
84
- process_instance.start
85
- end
93
+ process_id = process_instance.start
86
94
 
87
- configured_processes[pid] = configured_process
88
- forks[pid] = process_instance
95
+ configured_processes[process_id] = configured_process
96
+ process_instances[process_id] = process_instance
89
97
  end
90
98
 
91
- def set_procline
92
- procline "supervising #{supervised_processes.join(", ")}"
99
+ def check_and_replace_terminated_processes
93
100
  end
94
101
 
95
102
  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
103
+ SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: configured_processes.keys) do |payload|
104
+ perform_graceful_termination
102
105
 
103
- unless all_forks_terminated?
106
+ unless all_processes_terminated?
104
107
  payload[:shutdown_timeout_exceeded] = true
105
108
  terminate_immediately
106
109
  end
@@ -108,82 +111,37 @@ module SolidQueue
108
111
  end
109
112
 
110
113
  def terminate_immediately
111
- SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do
112
- quit_forks
114
+ SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: configured_processes.keys) do
115
+ perform_immediate_termination
113
116
  end
114
117
  end
115
118
 
116
- def shutdown
117
- SolidQueue.instrument(:shutdown_process, process: self) do
118
- run_callbacks(:shutdown) do
119
- stop_maintenance_task
120
- end
121
- end
122
- end
123
-
124
- def sync_std_streams
125
- STDOUT.sync = STDERR.sync = true
126
- end
127
-
128
- def supervised_processes
129
- forks.keys
119
+ def perform_graceful_termination
120
+ raise NotImplementedError
130
121
  end
131
122
 
132
- def term_forks
133
- signal_processes(forks.keys, :TERM)
123
+ def perform_immediate_termination
124
+ raise NotImplementedError
134
125
  end
135
126
 
136
- def quit_forks
137
- signal_processes(forks.keys, :QUIT)
127
+ def all_processes_terminated?
128
+ raise NotImplementedError
138
129
  end
139
130
 
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))
131
+ def shutdown
132
+ SolidQueue.instrument(:shutdown_process, process: self) do
133
+ run_callbacks(:shutdown) do
134
+ stop_maintenance_task
171
135
  end
172
136
  end
173
137
  end
174
138
 
175
- # When a supervised fork crashes or exits we need to mark all the
176
- # executions it had claimed as failed so that they can be retried
177
- # by some other worker.
178
- def handle_claimed_jobs_by(terminated_fork, status)
179
- if registered_process = SolidQueue::Process.find_by(name: terminated_fork.name)
180
- error = Processes::ProcessExitError.new(status)
181
- registered_process.fail_all_claimed_executions_with(error)
182
- end
139
+ def set_procline
140
+ procline "supervising #{configured_processes.keys.join(", ")}"
183
141
  end
184
142
 
185
- def all_forks_terminated?
186
- forks.empty?
143
+ def sync_std_streams
144
+ STDOUT.sync = STDERR.sync = true
187
145
  end
188
146
  end
189
147
  end
@@ -4,18 +4,18 @@ module SolidQueue
4
4
  module Timer
5
5
  extend self
6
6
 
7
- def wait_until(timeout, condition, &block)
7
+ def wait_until(timeout, condition)
8
8
  if timeout > 0
9
9
  deadline = monotonic_time_now + timeout
10
10
 
11
11
  while monotonic_time_now < deadline && !condition.call
12
12
  sleep 0.1
13
- block.call
13
+ yield if block_given?
14
14
  end
15
15
  else
16
16
  while !condition.call
17
17
  sleep 0.5
18
- block.call
18
+ yield if block_given?
19
19
  end
20
20
  end
21
21
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "1.2.1"
2
+ VERSION = "1.4.0"
3
3
  end
data/lib/solid_queue.rb CHANGED
@@ -43,6 +43,14 @@ module SolidQueue
43
43
 
44
44
  delegate :on_start, :on_stop, :on_exit, to: Supervisor
45
45
 
46
+ def schedule_recurring_task(key, **options)
47
+ RecurringTask.create_dynamic_task(key, **options)
48
+ end
49
+
50
+ def unschedule_recurring_task(key)
51
+ RecurringTask.delete_dynamic_task(key)
52
+ end
53
+
46
54
  [ Dispatcher, Scheduler, Worker ].each do |process|
47
55
  define_singleton_method(:"on_#{process.name.demodulize.downcase}_start") do |&block|
48
56
  process.on_start(&block)
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.2.1
4
+ version: 1.4.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: 2025-07-23 00:00:00.000000000 Z
11
+ date: 2026-03-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: 1.11.0
75
+ version: '1.11'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: 1.11.0
82
+ version: '1.11'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: thor
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '1.9'
125
+ - !ruby/object:Gem::Dependency
126
+ name: minitest
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '5.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '5.0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: mocha
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -140,16 +154,16 @@ dependencies:
140
154
  name: puma
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
- - - ">="
157
+ - - "~>"
144
158
  - !ruby/object:Gem::Version
145
- version: '0'
159
+ version: '7.0'
146
160
  type: :development
147
161
  prerelease: false
148
162
  version_requirements: !ruby/object:Gem::Requirement
149
163
  requirements:
150
- - - ">="
164
+ - - "~>"
151
165
  - !ruby/object:Gem::Version
152
- version: '0'
166
+ version: '7.0'
153
167
  - !ruby/object:Gem::Dependency
154
168
  name: mysql2
155
169
  requirement: !ruby/object:Gem::Requirement
@@ -257,7 +271,6 @@ extra_rdoc_files: []
257
271
  files:
258
272
  - MIT-LICENSE
259
273
  - README.md
260
- - Rakefile
261
274
  - UPGRADING.md
262
275
  - app/jobs/solid_queue/recurring_job.rb
263
276
  - app/models/solid_queue/blocked_execution.rb
@@ -298,11 +311,13 @@ files:
298
311
  - lib/puma/plugin/solid_queue.rb
299
312
  - lib/solid_queue.rb
300
313
  - lib/solid_queue/app_executor.rb
314
+ - lib/solid_queue/async_supervisor.rb
301
315
  - lib/solid_queue/cli.rb
302
316
  - lib/solid_queue/configuration.rb
303
317
  - lib/solid_queue/dispatcher.rb
304
318
  - lib/solid_queue/dispatcher/concurrency_maintenance.rb
305
319
  - lib/solid_queue/engine.rb
320
+ - lib/solid_queue/fork_supervisor.rb
306
321
  - lib/solid_queue/lifecycle_hooks.rb
307
322
  - lib/solid_queue/log_subscriber.rb
308
323
  - lib/solid_queue/pool.rb
@@ -317,6 +332,7 @@ files:
317
332
  - lib/solid_queue/processes/registrable.rb
318
333
  - lib/solid_queue/processes/runnable.rb
319
334
  - lib/solid_queue/processes/supervised.rb
335
+ - lib/solid_queue/processes/thread_terminated_error.rb
320
336
  - lib/solid_queue/scheduler.rb
321
337
  - lib/solid_queue/scheduler/recurring_schedule.rb
322
338
  - lib/solid_queue/supervisor.rb
data/Rakefile DELETED
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/setup"
4
-
5
- APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
6
- load "rails/tasks/engine.rake"
7
-
8
- load "rails/tasks/statistics.rake"
9
-
10
- require "bundler/gem_tasks"
11
- require "rake/tasklib"
12
-
13
- class TestHelpers < Rake::TaskLib
14
- def initialize(databases)
15
- @databases = databases
16
- define
17
- end
18
-
19
- def define
20
- desc "Run tests for all databases (mysql, postgres, sqlite)"
21
- task :test do
22
- @databases.each { |database| run_test_for_database(database) }
23
- end
24
-
25
- namespace :test do
26
- @databases.each do |database|
27
- desc "Run tests for #{database} database"
28
- task database do
29
- run_test_for_database(database)
30
- end
31
- end
32
- end
33
- end
34
-
35
- private
36
-
37
- def run_test_for_database(database)
38
- sh("TARGET_DB=#{database} bin/setup")
39
- sh("TARGET_DB=#{database} bin/rails test")
40
- end
41
- end
42
-
43
- TestHelpers.new(%w[ mysql postgres sqlite ])