solid_queue 0.4.1 → 0.7.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -25
  3. data/UPGRADING.md +102 -0
  4. data/app/jobs/solid_queue/recurring_job.rb +9 -0
  5. data/app/models/solid_queue/claimed_execution.rb +21 -8
  6. data/app/models/solid_queue/process/executor.rb +13 -1
  7. data/app/models/solid_queue/process/prunable.rb +8 -1
  8. data/app/models/solid_queue/process.rb +13 -6
  9. data/app/models/solid_queue/recurring_execution.rb +17 -4
  10. data/app/models/solid_queue/recurring_task/arguments.rb +17 -0
  11. data/app/models/solid_queue/recurring_task.rb +122 -0
  12. data/app/models/solid_queue/semaphore.rb +18 -5
  13. data/db/migrate/20240719134516_create_recurring_tasks.rb +20 -0
  14. data/db/migrate/20240811173327_add_name_to_processes.rb +5 -0
  15. data/db/migrate/20240813160053_make_name_not_null.rb +16 -0
  16. data/db/migrate/20240819165045_change_solid_queue_recurring_tasks_static_to_not_null.rb +5 -0
  17. data/lib/generators/solid_queue/install/USAGE +1 -0
  18. data/lib/generators/solid_queue/install/install_generator.rb +21 -7
  19. data/lib/generators/solid_queue/install/templates/jobs +6 -0
  20. data/lib/puma/plugin/solid_queue.rb +10 -32
  21. data/lib/solid_queue/cli.rb +20 -0
  22. data/lib/solid_queue/configuration.rb +40 -29
  23. data/lib/solid_queue/dispatcher/recurring_schedule.rb +21 -12
  24. data/lib/solid_queue/dispatcher.rb +8 -8
  25. data/lib/solid_queue/lifecycle_hooks.rb +43 -0
  26. data/lib/solid_queue/log_subscriber.rb +13 -6
  27. data/lib/solid_queue/processes/base.rb +11 -0
  28. data/lib/solid_queue/processes/poller.rb +8 -4
  29. data/lib/solid_queue/processes/process_exit_error.rb +20 -0
  30. data/lib/solid_queue/processes/process_missing_error.rb +9 -0
  31. data/lib/solid_queue/processes/process_pruned_error.rb +11 -0
  32. data/lib/solid_queue/processes/registrable.rb +1 -0
  33. data/lib/solid_queue/processes/runnable.rb +10 -16
  34. data/lib/solid_queue/supervisor/maintenance.rb +5 -3
  35. data/lib/solid_queue/supervisor.rb +126 -10
  36. data/lib/solid_queue/version.rb +1 -1
  37. data/lib/solid_queue/worker.rb +5 -0
  38. data/lib/solid_queue.rb +10 -0
  39. metadata +33 -7
  40. data/lib/solid_queue/dispatcher/recurring_task.rb +0 -99
  41. data/lib/solid_queue/supervisor/async_supervisor.rb +0 -44
  42. data/lib/solid_queue/supervisor/fork_supervisor.rb +0 -108
@@ -2,24 +2,33 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Supervisor < Processes::Base
5
- include Maintenance
5
+ include LifecycleHooks
6
+ include Maintenance, Signals, Pidfiled
6
7
 
7
8
  class << self
8
- def start(mode: :fork, load_configuration_from: nil)
9
+ def start(load_configuration_from: nil)
9
10
  SolidQueue.supervisor = true
10
- configuration = Configuration.new(mode: mode, load_from: load_configuration_from)
11
+ configuration = Configuration.new(load_from: load_configuration_from)
11
12
 
12
- klass = mode == :fork ? ForkSupervisor : AsyncSupervisor
13
- klass.new(configuration).tap(&:start)
13
+ if configuration.configured_processes.any?
14
+ new(configuration).tap(&:start)
15
+ else
16
+ abort "No workers or processed configured. Exiting..."
17
+ end
14
18
  end
15
19
  end
16
20
 
17
21
  def initialize(configuration)
18
22
  @configuration = configuration
23
+ @forks = {}
24
+ @configured_processes = {}
25
+
26
+ super
19
27
  end
20
28
 
21
29
  def start
22
30
  boot
31
+ run_start_hooks
23
32
 
24
33
  start_processes
25
34
  launch_maintenance_task
@@ -29,10 +38,11 @@ module SolidQueue
29
38
 
30
39
  def stop
31
40
  @stopped = true
41
+ run_stop_hooks
32
42
  end
33
43
 
34
44
  private
35
- attr_reader :configuration
45
+ attr_reader :configuration, :forks, :configured_processes
36
46
 
37
47
  def boot
38
48
  SolidQueue.instrument(:start_process, process: self) do
@@ -44,18 +54,66 @@ module SolidQueue
44
54
  end
45
55
 
46
56
  def start_processes
47
- configuration.processes.each { |configured_process| start_process(configured_process) }
57
+ configuration.configured_processes.each { |configured_process| start_process(configured_process) }
58
+ end
59
+
60
+ def supervise
61
+ loop do
62
+ break if stopped?
63
+
64
+ set_procline
65
+ process_signal_queue
66
+
67
+ unless stopped?
68
+ reap_and_replace_terminated_forks
69
+ interruptible_sleep(1.second)
70
+ end
71
+ end
72
+ ensure
73
+ shutdown
74
+ end
75
+
76
+ def start_process(configured_process)
77
+ process_instance = configured_process.instantiate.tap do |instance|
78
+ instance.supervised_by process
79
+ instance.mode = :fork
80
+ end
81
+
82
+ pid = fork do
83
+ process_instance.start
84
+ end
85
+
86
+ configured_processes[pid] = configured_process
87
+ forks[pid] = process_instance
48
88
  end
49
89
 
50
90
  def stopped?
51
91
  @stopped
52
92
  end
53
93
 
54
- def supervise
94
+ def set_procline
95
+ procline "supervising #{supervised_processes.join(", ")}"
55
96
  end
56
97
 
57
- def start_process(configured_process)
58
- raise NotImplementedError
98
+ def terminate_gracefully
99
+ SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do |payload|
100
+ term_forks
101
+
102
+ Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
103
+ reap_terminated_forks
104
+ end
105
+
106
+ unless all_forks_terminated?
107
+ payload[:shutdown_timeout_exceeded] = true
108
+ terminate_immediately
109
+ end
110
+ end
111
+ end
112
+
113
+ def terminate_immediately
114
+ SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: supervised_processes) do
115
+ quit_forks
116
+ end
59
117
  end
60
118
 
61
119
  def shutdown
@@ -69,5 +127,63 @@ module SolidQueue
69
127
  def sync_std_streams
70
128
  STDOUT.sync = STDERR.sync = true
71
129
  end
130
+
131
+ def supervised_processes
132
+ forks.keys
133
+ end
134
+
135
+ def term_forks
136
+ signal_processes(forks.keys, :TERM)
137
+ end
138
+
139
+ def quit_forks
140
+ signal_processes(forks.keys, :QUIT)
141
+ end
142
+
143
+ def reap_and_replace_terminated_forks
144
+ loop do
145
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
146
+ break unless pid
147
+
148
+ replace_fork(pid, status)
149
+ end
150
+ end
151
+
152
+ def reap_terminated_forks
153
+ loop do
154
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
155
+ break unless pid
156
+
157
+ if (terminated_fork = forks.delete(pid)) && (!status.exited? || status.exitstatus > 0)
158
+ handle_claimed_jobs_by(terminated_fork, status)
159
+ end
160
+
161
+ configured_processes.delete(pid)
162
+ end
163
+ rescue SystemCallError
164
+ # All children already reaped
165
+ end
166
+
167
+ def replace_fork(pid, status)
168
+ SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
169
+ if terminated_fork = forks.delete(pid)
170
+ payload[:fork] = terminated_fork
171
+ handle_claimed_jobs_by(terminated_fork, status)
172
+
173
+ start_process(configured_processes.delete(pid))
174
+ end
175
+ end
176
+ end
177
+
178
+ def handle_claimed_jobs_by(terminated_fork, status)
179
+ if registered_process = process.supervisees.find_by(name: terminated_fork.name)
180
+ error = Processes::ProcessExitError.new(status)
181
+ registered_process.fail_all_claimed_executions_with(error)
182
+ end
183
+ end
184
+
185
+ def all_forks_terminated?
186
+ forks.empty?
187
+ end
72
188
  end
73
189
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.4.1"
2
+ VERSION = "0.7.1"
3
3
  end
@@ -2,6 +2,11 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Worker < Processes::Poller
5
+ include LifecycleHooks
6
+
7
+ after_boot :run_start_hooks
8
+ before_shutdown :run_stop_hooks
9
+
5
10
  attr_accessor :queues, :pool
6
11
 
7
12
  def initialize(**options)
data/lib/solid_queue.rb CHANGED
@@ -43,6 +43,16 @@ module SolidQueue
43
43
  mattr_accessor :clear_finished_jobs_after, default: 1.day
44
44
  mattr_accessor :default_concurrency_control_period, default: 3.minutes
45
45
 
46
+ delegate :on_start, :on_stop, to: Supervisor
47
+
48
+ def on_worker_start(...)
49
+ Worker.on_start(...)
50
+ end
51
+
52
+ def on_worker_stop(...)
53
+ Worker.on_stop(...)
54
+ end
55
+
46
56
  def supervisor?
47
57
  supervisor
48
58
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rosa Gutierrez
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-08-05 00:00:00.000000000 Z
11
+ date: 2024-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 1.11.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: thor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.3.1
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.3.1
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: debug
85
99
  requirement: !ruby/object:Gem::Requirement
@@ -188,6 +202,8 @@ files:
188
202
  - MIT-LICENSE
189
203
  - README.md
190
204
  - Rakefile
205
+ - UPGRADING.md
206
+ - app/jobs/solid_queue/recurring_job.rb
191
207
  - app/models/solid_queue/blocked_execution.rb
192
208
  - app/models/solid_queue/claimed_execution.rb
193
209
  - app/models/solid_queue/execution.rb
@@ -210,39 +226,48 @@ files:
210
226
  - app/models/solid_queue/ready_execution.rb
211
227
  - app/models/solid_queue/record.rb
212
228
  - app/models/solid_queue/recurring_execution.rb
229
+ - app/models/solid_queue/recurring_task.rb
230
+ - app/models/solid_queue/recurring_task/arguments.rb
213
231
  - app/models/solid_queue/scheduled_execution.rb
214
232
  - app/models/solid_queue/semaphore.rb
215
233
  - config/routes.rb
216
234
  - db/migrate/20231211200639_create_solid_queue_tables.rb
217
235
  - db/migrate/20240110143450_add_missing_index_to_blocked_executions.rb
218
236
  - db/migrate/20240218110712_create_recurring_executions.rb
237
+ - db/migrate/20240719134516_create_recurring_tasks.rb
238
+ - db/migrate/20240811173327_add_name_to_processes.rb
239
+ - db/migrate/20240813160053_make_name_not_null.rb
240
+ - db/migrate/20240819165045_change_solid_queue_recurring_tasks_static_to_not_null.rb
219
241
  - lib/active_job/concurrency_controls.rb
220
242
  - lib/active_job/queue_adapters/solid_queue_adapter.rb
221
243
  - lib/generators/solid_queue/install/USAGE
222
244
  - lib/generators/solid_queue/install/install_generator.rb
223
245
  - lib/generators/solid_queue/install/templates/config.yml
246
+ - lib/generators/solid_queue/install/templates/jobs
224
247
  - lib/puma/plugin/solid_queue.rb
225
248
  - lib/solid_queue.rb
226
249
  - lib/solid_queue/app_executor.rb
250
+ - lib/solid_queue/cli.rb
227
251
  - lib/solid_queue/configuration.rb
228
252
  - lib/solid_queue/dispatcher.rb
229
253
  - lib/solid_queue/dispatcher/concurrency_maintenance.rb
230
254
  - lib/solid_queue/dispatcher/recurring_schedule.rb
231
- - lib/solid_queue/dispatcher/recurring_task.rb
232
255
  - lib/solid_queue/engine.rb
256
+ - lib/solid_queue/lifecycle_hooks.rb
233
257
  - lib/solid_queue/log_subscriber.rb
234
258
  - lib/solid_queue/pool.rb
235
259
  - lib/solid_queue/processes/base.rb
236
260
  - lib/solid_queue/processes/callbacks.rb
237
261
  - lib/solid_queue/processes/interruptible.rb
238
262
  - lib/solid_queue/processes/poller.rb
263
+ - lib/solid_queue/processes/process_exit_error.rb
264
+ - lib/solid_queue/processes/process_missing_error.rb
265
+ - lib/solid_queue/processes/process_pruned_error.rb
239
266
  - lib/solid_queue/processes/procline.rb
240
267
  - lib/solid_queue/processes/registrable.rb
241
268
  - lib/solid_queue/processes/runnable.rb
242
269
  - lib/solid_queue/processes/supervised.rb
243
270
  - lib/solid_queue/supervisor.rb
244
- - lib/solid_queue/supervisor/async_supervisor.rb
245
- - lib/solid_queue/supervisor/fork_supervisor.rb
246
271
  - lib/solid_queue/supervisor/maintenance.rb
247
272
  - lib/solid_queue/supervisor/pidfile.rb
248
273
  - lib/solid_queue/supervisor/pidfiled.rb
@@ -258,8 +283,9 @@ metadata:
258
283
  homepage_uri: https://github.com/rails/solid_queue
259
284
  source_code_uri: https://github.com/rails/solid_queue
260
285
  post_install_message: |
261
- Upgrading to Solid Queue 0.4.x? There are some breaking changes about how Solid Queue is started. Check
262
- https://github.com/rails/solid_queue/blob/main/UPGRADING.md for upgrade instructions.
286
+ Upgrading to Solid Queue 0.4.x, 0.5.x, 0.6.x or 0.7.x? There are some breaking changes about how Solid Queue is started,
287
+ configuration and new migrations. Check https://github.com/rails/solid_queue/blob/main/UPGRADING.md
288
+ for upgrade instructions.
263
289
  rdoc_options: []
264
290
  require_paths:
265
291
  - lib
@@ -1,99 +0,0 @@
1
- require "fugit"
2
-
3
- module SolidQueue
4
- class Dispatcher::RecurringTask
5
- class << self
6
- def wrap(args)
7
- args.is_a?(self) ? args : from_configuration(args.first, **args.second)
8
- end
9
-
10
- def from_configuration(key, **options)
11
- new(key, class_name: options[:class], schedule: options[:schedule], arguments: options[:args])
12
- end
13
- end
14
-
15
- attr_reader :key, :schedule, :class_name, :arguments
16
-
17
- def initialize(key, class_name:, schedule:, arguments: nil)
18
- @key = key
19
- @class_name = class_name
20
- @schedule = schedule
21
- @arguments = Array(arguments)
22
- end
23
-
24
- def delay_from_now
25
- [ (next_time - Time.current).to_f, 0 ].max
26
- end
27
-
28
- def next_time
29
- parsed_schedule.next_time.utc
30
- end
31
-
32
- def enqueue(at:)
33
- SolidQueue.instrument(:enqueue_recurring_task, task: key, at: at) do |payload|
34
- active_job = if using_solid_queue_adapter?
35
- perform_later_and_record(run_at: at)
36
- else
37
- payload[:other_adapter] = true
38
-
39
- perform_later do |job|
40
- unless job.successfully_enqueued?
41
- payload[:enqueue_error] = job.enqueue_error&.message
42
- end
43
- end
44
- end
45
-
46
- payload[:active_job_id] = active_job.job_id if active_job
47
- rescue RecurringExecution::AlreadyRecorded
48
- payload[:skipped] = true
49
- rescue Job::EnqueueError => error
50
- payload[:enqueue_error] = error.message
51
- end
52
- end
53
-
54
- def valid?
55
- parsed_schedule.instance_of?(Fugit::Cron)
56
- end
57
-
58
- def to_s
59
- "#{class_name}.perform_later(#{arguments.map(&:inspect).join(",")}) [ #{parsed_schedule.original} ]"
60
- end
61
-
62
- def to_h
63
- {
64
- schedule: schedule,
65
- class_name: class_name,
66
- arguments: arguments
67
- }
68
- end
69
-
70
- private
71
- def using_solid_queue_adapter?
72
- job_class.queue_adapter_name.inquiry.solid_queue?
73
- end
74
-
75
- def perform_later_and_record(run_at:)
76
- RecurringExecution.record(key, run_at) { perform_later }
77
- end
78
-
79
- def perform_later(&block)
80
- job_class.perform_later(*arguments_with_kwargs, &block)
81
- end
82
-
83
- def arguments_with_kwargs
84
- if arguments.last.is_a?(Hash)
85
- arguments[0...-1] + [ Hash.ruby2_keywords_hash(arguments.last) ]
86
- else
87
- arguments
88
- end
89
- end
90
-
91
- def parsed_schedule
92
- @parsed_schedule ||= Fugit.parse(schedule)
93
- end
94
-
95
- def job_class
96
- @job_class ||= class_name.safe_constantize
97
- end
98
- end
99
- end
@@ -1,44 +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
- configured_process.supervised_by process
27
- configured_process.start
28
-
29
- threads[configured_process.name] = configured_process
30
- end
31
-
32
- def stop_threads
33
- stop_threads = threads.values.map do |thr|
34
- Thread.new { thr.stop }
35
- end
36
-
37
- stop_threads.each { |thr| thr.join(SolidQueue.shutdown_timeout) }
38
- end
39
-
40
- def all_threads_terminated?
41
- threads.values.none?(&:alive?)
42
- end
43
- end
44
- end
@@ -1,108 +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
- @forks = {}
10
- end
11
-
12
- def kind
13
- "Supervisor(fork)"
14
- end
15
-
16
- private
17
- attr_reader :forks
18
-
19
- def supervise
20
- loop do
21
- break if stopped?
22
-
23
- procline "supervising #{forks.keys.join(", ")}"
24
- process_signal_queue
25
-
26
- unless stopped?
27
- reap_and_replace_terminated_forks
28
- interruptible_sleep(1.second)
29
- end
30
- end
31
- ensure
32
- shutdown
33
- end
34
-
35
- def start_process(configured_process)
36
- configured_process.supervised_by process
37
- configured_process.mode = :fork
38
-
39
- pid = fork do
40
- configured_process.start
41
- end
42
-
43
- forks[pid] = configured_process
44
- end
45
-
46
- def terminate_gracefully
47
- SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do |payload|
48
- term_forks
49
-
50
- Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
51
- reap_terminated_forks
52
- end
53
-
54
- unless all_forks_terminated?
55
- payload[:shutdown_timeout_exceeded] = true
56
- terminate_immediately
57
- end
58
- end
59
- end
60
-
61
- def terminate_immediately
62
- SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do
63
- quit_forks
64
- end
65
- end
66
-
67
- def term_forks
68
- signal_processes(forks.keys, :TERM)
69
- end
70
-
71
- def quit_forks
72
- signal_processes(forks.keys, :QUIT)
73
- end
74
-
75
- def reap_and_replace_terminated_forks
76
- loop do
77
- pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
78
- break unless pid
79
-
80
- replace_fork(pid, status)
81
- end
82
- end
83
-
84
- def reap_terminated_forks
85
- loop do
86
- pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
87
- break unless pid
88
-
89
- forks.delete(pid)
90
- end
91
- rescue SystemCallError
92
- # All children already reaped
93
- end
94
-
95
- def replace_fork(pid, status)
96
- SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
97
- if supervised_fork = forks.delete(pid)
98
- payload[:fork] = supervised_fork
99
- start_process(supervised_fork)
100
- end
101
- end
102
- end
103
-
104
- def all_forks_terminated?
105
- forks.empty?
106
- end
107
- end
108
- end