solid_queue 0.4.1 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
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