solid_queue 0.4.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -24
  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/log_subscriber.rb +13 -6
  26. data/lib/solid_queue/processes/base.rb +11 -0
  27. data/lib/solid_queue/processes/poller.rb +8 -4
  28. data/lib/solid_queue/processes/process_exit_error.rb +20 -0
  29. data/lib/solid_queue/processes/process_missing_error.rb +9 -0
  30. data/lib/solid_queue/processes/process_pruned_error.rb +11 -0
  31. data/lib/solid_queue/processes/registrable.rb +1 -0
  32. data/lib/solid_queue/processes/runnable.rb +0 -4
  33. data/lib/solid_queue/supervisor/maintenance.rb +5 -3
  34. data/lib/solid_queue/supervisor.rb +123 -10
  35. data/lib/solid_queue/version.rb +1 -1
  36. metadata +32 -7
  37. data/lib/solid_queue/dispatcher/recurring_task.rb +0 -99
  38. data/lib/solid_queue/supervisor/async_supervisor.rb +0 -44
  39. data/lib/solid_queue/supervisor/fork_supervisor.rb +0 -108
@@ -2,20 +2,27 @@
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
- klass = mode == :fork ? ForkSupervisor : AsyncSupervisor
13
- klass.new(configuration).tap(&:start)
12
+ if configuration.configured_processes.any?
13
+ new(configuration).tap(&:start)
14
+ else
15
+ abort "No workers or processed configured. Exiting..."
16
+ end
14
17
  end
15
18
  end
16
19
 
17
20
  def initialize(configuration)
18
21
  @configuration = configuration
22
+ @forks = {}
23
+ @configured_processes = {}
24
+
25
+ super
19
26
  end
20
27
 
21
28
  def start
@@ -32,7 +39,7 @@ module SolidQueue
32
39
  end
33
40
 
34
41
  private
35
- attr_reader :configuration
42
+ attr_reader :configuration, :forks, :configured_processes
36
43
 
37
44
  def boot
38
45
  SolidQueue.instrument(:start_process, process: self) do
@@ -44,18 +51,66 @@ module SolidQueue
44
51
  end
45
52
 
46
53
  def start_processes
47
- configuration.processes.each { |configured_process| start_process(configured_process) }
54
+ configuration.configured_processes.each { |configured_process| start_process(configured_process) }
55
+ end
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
48
85
  end
49
86
 
50
87
  def stopped?
51
88
  @stopped
52
89
  end
53
90
 
54
- def supervise
91
+ def set_procline
92
+ procline "supervising #{supervised_processes.join(", ")}"
55
93
  end
56
94
 
57
- def start_process(configured_process)
58
- 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
59
114
  end
60
115
 
61
116
  def shutdown
@@ -69,5 +124,63 @@ module SolidQueue
69
124
  def sync_std_streams
70
125
  STDOUT.sync = STDERR.sync = true
71
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
72
185
  end
73
186
  end
@@ -1,3 +1,3 @@
1
1
  module SolidQueue
2
- VERSION = "0.4.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.4.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-05 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,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,25 +226,32 @@ 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
233
256
  - lib/solid_queue/log_subscriber.rb
234
257
  - lib/solid_queue/pool.rb
@@ -236,13 +259,14 @@ files:
236
259
  - lib/solid_queue/processes/callbacks.rb
237
260
  - lib/solid_queue/processes/interruptible.rb
238
261
  - lib/solid_queue/processes/poller.rb
262
+ - lib/solid_queue/processes/process_exit_error.rb
263
+ - lib/solid_queue/processes/process_missing_error.rb
264
+ - lib/solid_queue/processes/process_pruned_error.rb
239
265
  - lib/solid_queue/processes/procline.rb
240
266
  - lib/solid_queue/processes/registrable.rb
241
267
  - lib/solid_queue/processes/runnable.rb
242
268
  - lib/solid_queue/processes/supervised.rb
243
269
  - lib/solid_queue/supervisor.rb
244
- - lib/solid_queue/supervisor/async_supervisor.rb
245
- - lib/solid_queue/supervisor/fork_supervisor.rb
246
270
  - lib/solid_queue/supervisor/maintenance.rb
247
271
  - lib/solid_queue/supervisor/pidfile.rb
248
272
  - lib/solid_queue/supervisor/pidfiled.rb
@@ -258,8 +282,9 @@ metadata:
258
282
  homepage_uri: https://github.com/rails/solid_queue
259
283
  source_code_uri: https://github.com/rails/solid_queue
260
284
  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.
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.
263
288
  rdoc_options: []
264
289
  require_paths:
265
290
  - 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