solid_queue 0.4.1 → 0.7.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 (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