solid_queue 1.2.4 → 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.
- checksums.yaml +4 -4
- data/README.md +99 -19
- data/app/models/solid_queue/blocked_execution.rb +3 -1
- data/app/models/solid_queue/failed_execution.rb +1 -1
- data/app/models/solid_queue/job/concurrency_controls.rb +1 -1
- data/app/models/solid_queue/job/retryable.rb +10 -1
- data/app/models/solid_queue/ready_execution.rb +2 -1
- data/app/models/solid_queue/record.rb +7 -0
- data/app/models/solid_queue/recurring_task.rb +25 -7
- data/app/models/solid_queue/semaphore.rb +1 -1
- data/lib/puma/plugin/solid_queue.rb +70 -22
- data/lib/solid_queue/app_executor.rb +10 -0
- data/lib/solid_queue/async_supervisor.rb +52 -0
- data/lib/solid_queue/cli.rb +4 -0
- data/lib/solid_queue/configuration.rb +41 -8
- data/lib/solid_queue/dispatcher.rb +1 -0
- data/lib/solid_queue/fork_supervisor.rb +68 -0
- data/lib/solid_queue/processes/registrable.rb +5 -1
- data/lib/solid_queue/processes/runnable.rb +25 -18
- data/lib/solid_queue/processes/thread_terminated_error.rb +11 -0
- data/lib/solid_queue/scheduler/recurring_schedule.rb +61 -11
- data/lib/solid_queue/scheduler.rb +23 -4
- data/lib/solid_queue/supervisor/maintenance.rb +11 -0
- data/lib/solid_queue/supervisor/signals.rb +2 -2
- data/lib/solid_queue/supervisor.rb +40 -84
- data/lib/solid_queue/timer.rb +3 -3
- data/lib/solid_queue/version.rb +1 -1
- data/lib/solid_queue.rb +8 -0
- metadata +19 -2
|
@@ -28,6 +28,11 @@ module SolidQueue
|
|
|
28
28
|
concurrency_maintenance_interval: 600
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
SCHEDULER_DEFAULTS = {
|
|
32
|
+
polling_interval: 5,
|
|
33
|
+
dynamic_tasks_enabled: false
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
DEFAULT_CONFIG_FILE_PATH = "config/queue.yml"
|
|
32
37
|
DEFAULT_RECURRING_SCHEDULE_FILE_PATH = "config/recurring.yml"
|
|
33
38
|
|
|
@@ -56,6 +61,14 @@ module SolidQueue
|
|
|
56
61
|
end
|
|
57
62
|
end
|
|
58
63
|
|
|
64
|
+
def mode
|
|
65
|
+
@options[:mode].to_s.inquiry
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def standalone?
|
|
69
|
+
mode.fork? || @options[:standalone]
|
|
70
|
+
end
|
|
71
|
+
|
|
59
72
|
private
|
|
60
73
|
attr_reader :options
|
|
61
74
|
|
|
@@ -84,6 +97,8 @@ module SolidQueue
|
|
|
84
97
|
|
|
85
98
|
def default_options
|
|
86
99
|
{
|
|
100
|
+
mode: ENV["SOLID_QUEUE_SUPERVISOR_MODE"] || :fork,
|
|
101
|
+
standalone: true,
|
|
87
102
|
config_file: Rails.root.join(ENV["SOLID_QUEUE_CONFIG"] || DEFAULT_CONFIG_FILE_PATH),
|
|
88
103
|
recurring_schedule_file: Rails.root.join(ENV["SOLID_QUEUE_RECURRING_SCHEDULE"] || DEFAULT_RECURRING_SCHEDULE_FILE_PATH),
|
|
89
104
|
only_work: false,
|
|
@@ -110,7 +125,12 @@ module SolidQueue
|
|
|
110
125
|
|
|
111
126
|
def workers
|
|
112
127
|
workers_options.flat_map do |worker_options|
|
|
113
|
-
processes =
|
|
128
|
+
processes = if mode.fork?
|
|
129
|
+
worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
|
|
130
|
+
else
|
|
131
|
+
1
|
|
132
|
+
end
|
|
133
|
+
|
|
114
134
|
processes.times.map { Process.new(:worker, worker_options.with_defaults(WORKER_DEFAULTS)) }
|
|
115
135
|
end
|
|
116
136
|
end
|
|
@@ -122,8 +142,10 @@ module SolidQueue
|
|
|
122
142
|
end
|
|
123
143
|
|
|
124
144
|
def schedulers
|
|
125
|
-
if
|
|
126
|
-
|
|
145
|
+
return [] if skip_recurring_tasks?
|
|
146
|
+
|
|
147
|
+
if recurring_tasks.any? || dynamic_recurring_tasks_enabled?
|
|
148
|
+
[ Process.new(:scheduler, { recurring_tasks: recurring_tasks, **scheduler_options.with_defaults(SCHEDULER_DEFAULTS) }) ]
|
|
127
149
|
else
|
|
128
150
|
[]
|
|
129
151
|
end
|
|
@@ -139,17 +161,29 @@ module SolidQueue
|
|
|
139
161
|
.map { |options| options.dup.symbolize_keys }
|
|
140
162
|
end
|
|
141
163
|
|
|
164
|
+
def scheduler_options
|
|
165
|
+
@scheduler_options ||= processes_config.fetch(:scheduler, {}).dup.symbolize_keys
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def dynamic_recurring_tasks_enabled?
|
|
169
|
+
scheduler_options.fetch(:dynamic_tasks_enabled, SCHEDULER_DEFAULTS[:dynamic_tasks_enabled])
|
|
170
|
+
end
|
|
171
|
+
|
|
142
172
|
def recurring_tasks
|
|
143
173
|
@recurring_tasks ||= recurring_tasks_config.map do |id, options|
|
|
144
|
-
RecurringTask.from_configuration(id, **options) if options&.has_key?(:schedule)
|
|
174
|
+
RecurringTask.from_configuration(id, **options.merge(static: true)) if options&.has_key?(:schedule)
|
|
145
175
|
end.compact
|
|
146
176
|
end
|
|
147
177
|
|
|
148
178
|
def processes_config
|
|
149
179
|
@processes_config ||= config_from \
|
|
150
|
-
options.slice(:workers, :dispatchers).presence || options[:config_file],
|
|
151
|
-
keys: [ :workers, :dispatchers ],
|
|
152
|
-
fallback: {
|
|
180
|
+
options.slice(:workers, :dispatchers, :scheduler).presence || options[:config_file],
|
|
181
|
+
keys: [ :workers, :dispatchers, :scheduler ],
|
|
182
|
+
fallback: {
|
|
183
|
+
workers: [ WORKER_DEFAULTS ],
|
|
184
|
+
dispatchers: [ DISPATCHER_DEFAULTS ],
|
|
185
|
+
scheduler: SCHEDULER_DEFAULTS
|
|
186
|
+
}
|
|
153
187
|
end
|
|
154
188
|
|
|
155
189
|
def recurring_tasks_config
|
|
@@ -158,7 +192,6 @@ module SolidQueue
|
|
|
158
192
|
end
|
|
159
193
|
end
|
|
160
194
|
|
|
161
|
-
|
|
162
195
|
def config_from(file_or_hash, keys: [], fallback: {}, env: Rails.env)
|
|
163
196
|
load_config_from(file_or_hash).then do |config|
|
|
164
197
|
config = config[env.to_sym] ? config[env.to_sym] : config
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class ForkSupervisor < Supervisor
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def perform_graceful_termination
|
|
8
|
+
term_forks
|
|
9
|
+
|
|
10
|
+
Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_processes_terminated? }) do
|
|
11
|
+
reap_terminated_forks
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def perform_immediate_termination
|
|
16
|
+
quit_forks
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def term_forks
|
|
20
|
+
signal_processes(process_instances.keys, :TERM)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def quit_forks
|
|
24
|
+
signal_processes(process_instances.keys, :QUIT)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def check_and_replace_terminated_processes
|
|
28
|
+
loop do
|
|
29
|
+
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
30
|
+
break unless pid
|
|
31
|
+
|
|
32
|
+
replace_fork(pid, status)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reap_terminated_forks
|
|
37
|
+
loop do
|
|
38
|
+
pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
|
|
39
|
+
break unless pid
|
|
40
|
+
|
|
41
|
+
if (terminated_fork = process_instances.delete(pid)) && (!status.exited? || status.exitstatus.to_i > 0)
|
|
42
|
+
error = Processes::ProcessExitError.new(status)
|
|
43
|
+
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
configured_processes.delete(pid)
|
|
47
|
+
end
|
|
48
|
+
rescue SystemCallError
|
|
49
|
+
# All children already reaped
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def replace_fork(pid, status)
|
|
53
|
+
SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
|
|
54
|
+
if terminated_fork = process_instances.delete(pid)
|
|
55
|
+
payload[:fork] = terminated_fork
|
|
56
|
+
error = Processes::ProcessExitError.new(status)
|
|
57
|
+
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
58
|
+
|
|
59
|
+
start_process(configured_processes.delete(pid))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def all_processes_terminated?
|
|
65
|
+
process_instances.empty?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -54,10 +54,14 @@ module SolidQueue::Processes
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def heartbeat
|
|
57
|
-
process
|
|
57
|
+
process&.heartbeat
|
|
58
58
|
rescue ActiveRecord::RecordNotFound
|
|
59
59
|
self.process = nil
|
|
60
60
|
wake_up
|
|
61
61
|
end
|
|
62
|
+
|
|
63
|
+
def reload_metadata
|
|
64
|
+
wrap_in_app_executor { process&.update(metadata: metadata.compact) }
|
|
65
|
+
end
|
|
62
66
|
end
|
|
63
67
|
end
|
|
@@ -7,20 +7,26 @@ module SolidQueue::Processes
|
|
|
7
7
|
attr_writer :mode
|
|
8
8
|
|
|
9
9
|
def start
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
if running_async?
|
|
13
|
-
@thread = create_thread { run }
|
|
14
|
-
else
|
|
10
|
+
run_in_mode do
|
|
11
|
+
boot
|
|
15
12
|
run
|
|
16
13
|
end
|
|
17
14
|
end
|
|
18
15
|
|
|
19
16
|
def stop
|
|
20
17
|
super
|
|
21
|
-
|
|
22
18
|
wake_up
|
|
23
|
-
|
|
19
|
+
|
|
20
|
+
# When not supervised, block until the thread terminates for backward
|
|
21
|
+
# compatibility with code that expects stop to be synchronous.
|
|
22
|
+
# When supervised, the supervisor controls the shutdown timeout.
|
|
23
|
+
unless supervised?
|
|
24
|
+
@thread&.join
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def alive?
|
|
29
|
+
!running_async? || @thread&.alive?
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
private
|
|
@@ -30,6 +36,18 @@ module SolidQueue::Processes
|
|
|
30
36
|
(@mode || DEFAULT_MODE).to_s.inquiry
|
|
31
37
|
end
|
|
32
38
|
|
|
39
|
+
def run_in_mode(&block)
|
|
40
|
+
case
|
|
41
|
+
when running_as_fork?
|
|
42
|
+
fork(&block)
|
|
43
|
+
when running_async?
|
|
44
|
+
@thread = create_thread(&block)
|
|
45
|
+
@thread.object_id
|
|
46
|
+
else
|
|
47
|
+
block.call
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
33
51
|
def boot
|
|
34
52
|
SolidQueue.instrument(:start_process, process: self) do
|
|
35
53
|
run_callbacks(:boot) do
|
|
@@ -74,16 +92,5 @@ module SolidQueue::Processes
|
|
|
74
92
|
def running_as_fork?
|
|
75
93
|
mode.fork?
|
|
76
94
|
end
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def create_thread(&block)
|
|
80
|
-
Thread.new do
|
|
81
|
-
Thread.current.name = name
|
|
82
|
-
block.call
|
|
83
|
-
rescue Exception => exception
|
|
84
|
-
handle_thread_error(exception)
|
|
85
|
-
raise
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
95
|
end
|
|
89
96
|
end
|
|
@@ -4,21 +4,28 @@ module SolidQueue
|
|
|
4
4
|
class Scheduler::RecurringSchedule
|
|
5
5
|
include AppExecutor
|
|
6
6
|
|
|
7
|
-
attr_reader :
|
|
7
|
+
attr_reader :scheduled_tasks
|
|
8
|
+
|
|
9
|
+
def initialize(static_tasks, dynamic_tasks_enabled: false)
|
|
10
|
+
@static_tasks = Array(static_tasks).map { |task| RecurringTask.wrap(task) }.select(&:valid?)
|
|
11
|
+
@dynamic_tasks_enabled = dynamic_tasks_enabled
|
|
8
12
|
|
|
9
|
-
def initialize(tasks)
|
|
10
|
-
@configured_tasks = Array(tasks).map { |task| SolidQueue::RecurringTask.wrap(task) }.select(&:valid?)
|
|
11
13
|
@scheduled_tasks = Concurrent::Hash.new
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
def configured_tasks
|
|
17
|
+
static_tasks + dynamic_tasks
|
|
18
|
+
end
|
|
19
|
+
|
|
14
20
|
def empty?
|
|
15
|
-
|
|
21
|
+
scheduled_tasks.empty? && dynamic_tasks.empty?
|
|
16
22
|
end
|
|
17
23
|
|
|
18
24
|
def schedule_tasks
|
|
19
25
|
wrap_in_app_executor do
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
persist_static_tasks
|
|
27
|
+
reload_static_tasks
|
|
28
|
+
reload_dynamic_tasks
|
|
22
29
|
end
|
|
23
30
|
|
|
24
31
|
configured_tasks.each do |task|
|
|
@@ -39,14 +46,57 @@ module SolidQueue
|
|
|
39
46
|
configured_tasks.map(&:key)
|
|
40
47
|
end
|
|
41
48
|
|
|
49
|
+
def reschedule_dynamic_tasks
|
|
50
|
+
wrap_in_app_executor do
|
|
51
|
+
reload_dynamic_tasks
|
|
52
|
+
schedule_created_dynamic_tasks
|
|
53
|
+
unschedule_deleted_dynamic_tasks
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
42
57
|
private
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
attr_reader :static_tasks
|
|
59
|
+
|
|
60
|
+
def static_task_keys
|
|
61
|
+
static_tasks.map(&:key)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dynamic_tasks
|
|
65
|
+
@dynamic_tasks ||= load_dynamic_tasks
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def dynamic_tasks_enabled?
|
|
69
|
+
@dynamic_tasks_enabled
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def schedule_created_dynamic_tasks
|
|
73
|
+
RecurringTask.dynamic.where.not(key: scheduled_tasks.keys).each do |task|
|
|
74
|
+
schedule_task(task)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def unschedule_deleted_dynamic_tasks
|
|
79
|
+
(scheduled_tasks.keys - RecurringTask.pluck(:key)).each do |key|
|
|
80
|
+
scheduled_tasks[key].cancel
|
|
81
|
+
scheduled_tasks.delete(key)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def persist_static_tasks
|
|
86
|
+
RecurringTask.static.where.not(key: static_task_keys).delete_all
|
|
87
|
+
RecurringTask.create_or_update_all static_tasks
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def reload_static_tasks
|
|
91
|
+
@static_tasks = RecurringTask.static.where(key: static_task_keys).to_a
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def reload_dynamic_tasks
|
|
95
|
+
@dynamic_tasks = load_dynamic_tasks
|
|
46
96
|
end
|
|
47
97
|
|
|
48
|
-
def
|
|
49
|
-
|
|
98
|
+
def load_dynamic_tasks
|
|
99
|
+
dynamic_tasks_enabled? ? RecurringTask.dynamic.to_a : []
|
|
50
100
|
end
|
|
51
101
|
|
|
52
102
|
def schedule(task)
|
|
@@ -5,7 +5,7 @@ module SolidQueue
|
|
|
5
5
|
include Processes::Runnable
|
|
6
6
|
include LifecycleHooks
|
|
7
7
|
|
|
8
|
-
attr_reader :recurring_schedule
|
|
8
|
+
attr_reader :recurring_schedule, :polling_interval
|
|
9
9
|
|
|
10
10
|
after_boot :run_start_hooks
|
|
11
11
|
after_boot :schedule_recurring_tasks
|
|
@@ -14,7 +14,10 @@ module SolidQueue
|
|
|
14
14
|
after_shutdown :run_exit_hooks
|
|
15
15
|
|
|
16
16
|
def initialize(recurring_tasks:, **options)
|
|
17
|
-
|
|
17
|
+
options = options.dup.with_defaults(SolidQueue::Configuration::SCHEDULER_DEFAULTS)
|
|
18
|
+
@dynamic_tasks_enabled = options[:dynamic_tasks_enabled]
|
|
19
|
+
@polling_interval = options[:polling_interval]
|
|
20
|
+
@recurring_schedule = RecurringSchedule.new(recurring_tasks, dynamic_tasks_enabled: @dynamic_tasks_enabled)
|
|
18
21
|
|
|
19
22
|
super(**options)
|
|
20
23
|
end
|
|
@@ -24,13 +27,16 @@ module SolidQueue
|
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
private
|
|
27
|
-
|
|
30
|
+
|
|
31
|
+
STATIC_SLEEP_INTERVAL = 60
|
|
28
32
|
|
|
29
33
|
def run
|
|
30
34
|
loop do
|
|
31
35
|
break if shutting_down?
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
reload_dynamic_schedule if dynamic_tasks_enabled?
|
|
38
|
+
|
|
39
|
+
interruptible_sleep(sleep_interval)
|
|
34
40
|
end
|
|
35
41
|
ensure
|
|
36
42
|
SolidQueue.instrument(:shutdown_process, process: self) do
|
|
@@ -46,10 +52,23 @@ module SolidQueue
|
|
|
46
52
|
recurring_schedule.unschedule_tasks
|
|
47
53
|
end
|
|
48
54
|
|
|
55
|
+
def reload_dynamic_schedule
|
|
56
|
+
recurring_schedule.reschedule_dynamic_tasks
|
|
57
|
+
reload_metadata
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def dynamic_tasks_enabled?
|
|
61
|
+
@dynamic_tasks_enabled
|
|
62
|
+
end
|
|
63
|
+
|
|
49
64
|
def all_work_completed?
|
|
50
65
|
recurring_schedule.empty?
|
|
51
66
|
end
|
|
52
67
|
|
|
68
|
+
def sleep_interval
|
|
69
|
+
dynamic_tasks_enabled? ? polling_interval : STATIC_SLEEP_INTERVAL
|
|
70
|
+
end
|
|
71
|
+
|
|
53
72
|
def set_procline
|
|
54
73
|
procline "scheduling #{recurring_schedule.task_keys.join(",")}"
|
|
55
74
|
end
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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, :
|
|
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
|
-
|
|
66
|
-
|
|
73
|
+
if standalone?
|
|
74
|
+
set_procline
|
|
75
|
+
process_signal_queue
|
|
76
|
+
end
|
|
67
77
|
|
|
68
78
|
unless stopped?
|
|
69
|
-
|
|
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 =
|
|
90
|
+
instance.mode = mode
|
|
81
91
|
end
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
process_instance.start
|
|
85
|
-
end
|
|
93
|
+
process_id = process_instance.start
|
|
86
94
|
|
|
87
|
-
configured_processes[
|
|
88
|
-
|
|
95
|
+
configured_processes[process_id] = configured_process
|
|
96
|
+
process_instances[process_id] = process_instance
|
|
89
97
|
end
|
|
90
98
|
|
|
91
|
-
def
|
|
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:
|
|
97
|
-
|
|
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
|
|
106
|
+
unless all_processes_terminated?
|
|
104
107
|
payload[:shutdown_timeout_exceeded] = true
|
|
105
108
|
terminate_immediately
|
|
106
109
|
end
|
|
@@ -108,84 +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:
|
|
112
|
-
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def shutdown
|
|
117
|
-
SolidQueue.instrument(:shutdown_process, process: self) do
|
|
118
|
-
run_callbacks(:shutdown) do
|
|
119
|
-
stop_maintenance_task
|
|
120
|
-
end
|
|
114
|
+
SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: configured_processes.keys) do
|
|
115
|
+
perform_immediate_termination
|
|
121
116
|
end
|
|
122
117
|
end
|
|
123
118
|
|
|
124
|
-
def
|
|
125
|
-
|
|
119
|
+
def perform_graceful_termination
|
|
120
|
+
raise NotImplementedError
|
|
126
121
|
end
|
|
127
122
|
|
|
128
|
-
def
|
|
129
|
-
|
|
123
|
+
def perform_immediate_termination
|
|
124
|
+
raise NotImplementedError
|
|
130
125
|
end
|
|
131
126
|
|
|
132
|
-
def
|
|
133
|
-
|
|
127
|
+
def all_processes_terminated?
|
|
128
|
+
raise NotImplementedError
|
|
134
129
|
end
|
|
135
130
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
|
|
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))
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
# by some other worker.
|
|
178
|
-
def handle_claimed_jobs_by(terminated_fork, status)
|
|
179
|
-
wrap_in_app_executor do
|
|
180
|
-
if registered_process = SolidQueue::Process.find_by(name: terminated_fork.name)
|
|
181
|
-
error = Processes::ProcessExitError.new(status)
|
|
182
|
-
registered_process.fail_all_claimed_executions_with(error)
|
|
183
|
-
end
|
|
184
|
-
end
|
|
139
|
+
def set_procline
|
|
140
|
+
procline "supervising #{configured_processes.keys.join(", ")}"
|
|
185
141
|
end
|
|
186
142
|
|
|
187
|
-
def
|
|
188
|
-
|
|
143
|
+
def sync_std_streams
|
|
144
|
+
STDOUT.sync = STDERR.sync = true
|
|
189
145
|
end
|
|
190
146
|
end
|
|
191
147
|
end
|
data/lib/solid_queue/timer.rb
CHANGED
|
@@ -4,18 +4,18 @@ module SolidQueue
|
|
|
4
4
|
module Timer
|
|
5
5
|
extend self
|
|
6
6
|
|
|
7
|
-
def wait_until(timeout, condition
|
|
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
|
-
|
|
13
|
+
yield if block_given?
|
|
14
14
|
end
|
|
15
15
|
else
|
|
16
16
|
while !condition.call
|
|
17
17
|
sleep 0.5
|
|
18
|
-
|
|
18
|
+
yield if block_given?
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
data/lib/solid_queue/version.rb
CHANGED