solid_queue 1.2.1 → 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 +112 -32
- data/app/models/solid_queue/blocked_execution.rb +3 -1
- data/app/models/solid_queue/claimed_execution.rb +4 -2
- data/app/models/solid_queue/failed_execution.rb +6 -3
- 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/job.rb +1 -0
- data/app/models/solid_queue/ready_execution.rb +2 -1
- data/app/models/solid_queue/record.rb +20 -5
- data/app/models/solid_queue/recurring_execution.rb +1 -1
- data/app/models/solid_queue/recurring_task.rb +27 -9
- data/app/models/solid_queue/semaphore.rb +2 -2
- data/lib/puma/plugin/solid_queue.rb +74 -14
- data/lib/solid_queue/app_executor.rb +10 -0
- data/lib/solid_queue/async_supervisor.rb +52 -0
- data/lib/solid_queue/cli.rb +5 -1
- data/lib/solid_queue/configuration.rb +42 -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 +15 -9
- 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 -82
- data/lib/solid_queue/timer.rb +3 -3
- data/lib/solid_queue/version.rb +1 -1
- data/lib/solid_queue.rb +8 -0
- metadata +25 -9
- data/Rakefile +0 -43
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
require "puma/plugin"
|
|
2
2
|
|
|
3
|
+
module Puma
|
|
4
|
+
class DSL
|
|
5
|
+
def solid_queue_mode(mode = :fork)
|
|
6
|
+
@options[:solid_queue_mode] = mode.to_sym
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
3
11
|
Puma::Plugin.create do
|
|
4
12
|
attr_reader :puma_pid, :solid_queue_pid, :log_writer, :solid_queue_supervisor
|
|
5
13
|
|
|
@@ -7,26 +15,78 @@ Puma::Plugin.create do
|
|
|
7
15
|
@log_writer = launcher.log_writer
|
|
8
16
|
@puma_pid = $$
|
|
9
17
|
|
|
10
|
-
|
|
11
|
-
|
|
18
|
+
if launcher.options[:solid_queue_mode] == :async
|
|
19
|
+
start_async(launcher)
|
|
20
|
+
else
|
|
21
|
+
start_forked(launcher)
|
|
12
22
|
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
def start_forked(launcher)
|
|
27
|
+
in_background do
|
|
28
|
+
monitor_solid_queue
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new("7")
|
|
32
|
+
launcher.events.on_booted do
|
|
33
|
+
@solid_queue_pid = fork do
|
|
34
|
+
Thread.new { monitor_puma }
|
|
35
|
+
SolidQueue::Supervisor.start(mode: :fork)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
13
38
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
39
|
+
launcher.events.on_stopped { stop_solid_queue_fork }
|
|
40
|
+
launcher.events.on_restart { stop_solid_queue_fork }
|
|
41
|
+
else
|
|
42
|
+
launcher.events.after_booted do
|
|
43
|
+
@solid_queue_pid = fork do
|
|
44
|
+
Thread.new { monitor_puma }
|
|
45
|
+
start_solid_queue(mode: :fork)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
launcher.events.after_stopped { stop_solid_queue_fork }
|
|
50
|
+
launcher.events.before_restart { stop_solid_queue_fork }
|
|
18
51
|
end
|
|
19
52
|
end
|
|
20
53
|
|
|
21
|
-
launcher
|
|
22
|
-
|
|
23
|
-
|
|
54
|
+
def start_async(launcher)
|
|
55
|
+
if Gem::Version.new(Puma::Const::VERSION) < Gem::Version.new("7")
|
|
56
|
+
launcher.events.on_booted do
|
|
57
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
launcher.events.on_stopped { solid_queue_supervisor&.stop }
|
|
61
|
+
|
|
62
|
+
launcher.events.on_restart do
|
|
63
|
+
solid_queue_supervisor&.stop
|
|
64
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
launcher.events.after_booted do
|
|
68
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
launcher.events.after_stopped { solid_queue_supervisor&.stop }
|
|
72
|
+
|
|
73
|
+
launcher.events.before_restart do
|
|
74
|
+
solid_queue_supervisor&.stop
|
|
75
|
+
start_solid_queue(mode: :async, standalone: false)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def start_solid_queue(**options)
|
|
81
|
+
@solid_queue_supervisor = SolidQueue::Supervisor.start(**options)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def stop_solid_queue_fork
|
|
85
|
+
return unless solid_queue_pid
|
|
24
86
|
|
|
25
|
-
private
|
|
26
|
-
def stop_solid_queue
|
|
27
87
|
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
|
28
88
|
log "Stopping Solid Queue..."
|
|
29
|
-
Process.kill(:INT, solid_queue_pid)
|
|
89
|
+
Process.kill(:INT, solid_queue_pid)
|
|
30
90
|
Process.wait(solid_queue_pid)
|
|
31
91
|
rescue Errno::ECHILD, Errno::ESRCH
|
|
32
92
|
end
|
|
@@ -36,7 +96,7 @@ Puma::Plugin.create do
|
|
|
36
96
|
end
|
|
37
97
|
|
|
38
98
|
def monitor_solid_queue
|
|
39
|
-
monitor(:
|
|
99
|
+
monitor(:solid_queue_fork_dead?, "Detected Solid Queue has gone away, stopping Puma...")
|
|
40
100
|
end
|
|
41
101
|
|
|
42
102
|
def monitor(process_dead, message)
|
|
@@ -50,7 +110,7 @@ Puma::Plugin.create do
|
|
|
50
110
|
end
|
|
51
111
|
end
|
|
52
112
|
|
|
53
|
-
def
|
|
113
|
+
def solid_queue_fork_dead?
|
|
54
114
|
if solid_queue_started?
|
|
55
115
|
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
|
56
116
|
end
|
|
@@ -17,5 +17,15 @@ module SolidQueue
|
|
|
17
17
|
SolidQueue.on_thread_error.call(error)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
def create_thread(&block)
|
|
22
|
+
Thread.new do
|
|
23
|
+
Thread.current.name = name
|
|
24
|
+
block.call
|
|
25
|
+
rescue Exception => exception
|
|
26
|
+
handle_thread_error(exception)
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
end
|
|
20
30
|
end
|
|
21
31
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class AsyncSupervisor < Supervisor
|
|
5
|
+
after_shutdown :terminate_gracefully, unless: :standalone?
|
|
6
|
+
|
|
7
|
+
def stop
|
|
8
|
+
super
|
|
9
|
+
@thread&.join
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
def supervise
|
|
14
|
+
if standalone? then super
|
|
15
|
+
else
|
|
16
|
+
@thread = create_thread { super }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check_and_replace_terminated_processes
|
|
21
|
+
terminated_threads = process_instances.select { |thread_id, instance| !instance.alive? }
|
|
22
|
+
terminated_threads.each { |thread_id, _| replace_thread(thread_id) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def replace_thread(thread_id)
|
|
26
|
+
SolidQueue.instrument(:replace_thread, supervisor_pid: ::Process.pid) do |payload|
|
|
27
|
+
if (instance = process_instances.delete(thread_id))
|
|
28
|
+
payload[:thread] = instance
|
|
29
|
+
|
|
30
|
+
error = Processes::ThreadTerminatedError.new(instance.name)
|
|
31
|
+
release_claimed_jobs_by(instance, with_error: error)
|
|
32
|
+
|
|
33
|
+
start_process(configured_processes.delete(thread_id))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def perform_graceful_termination
|
|
39
|
+
process_instances.values.each(&:stop)
|
|
40
|
+
|
|
41
|
+
Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_processes_terminated? })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def perform_immediate_termination
|
|
45
|
+
exit!
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def all_processes_terminated?
|
|
49
|
+
process_instances.values.none?(&:alive?)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/solid_queue/cli.rb
CHANGED
|
@@ -8,11 +8,15 @@ module SolidQueue
|
|
|
8
8
|
desc: "Path to config file (default: #{Configuration::DEFAULT_CONFIG_FILE_PATH}).",
|
|
9
9
|
banner: "SOLID_QUEUE_CONFIG"
|
|
10
10
|
|
|
11
|
+
class_option :mode, type: :string, enum: %w[ fork async ],
|
|
12
|
+
desc: "Whether to fork processes for workers and dispatchers (fork) or to run these in the same process as the supervisor (async) (default: fork).",
|
|
13
|
+
banner: "SOLID_QUEUE_SUPERVISOR_MODE"
|
|
14
|
+
|
|
11
15
|
class_option :recurring_schedule_file, type: :string,
|
|
12
16
|
desc: "Path to recurring schedule definition (default: #{Configuration::DEFAULT_RECURRING_SCHEDULE_FILE_PATH}).",
|
|
13
17
|
banner: "SOLID_QUEUE_RECURRING_SCHEDULE"
|
|
14
18
|
|
|
15
|
-
class_option :skip_recurring, type: :boolean,
|
|
19
|
+
class_option :skip_recurring, type: :boolean,
|
|
16
20
|
desc: "Whether to skip recurring tasks scheduling",
|
|
17
21
|
banner: "SOLID_QUEUE_SKIP_RECURRING"
|
|
18
22
|
|
|
@@ -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
|
|
@@ -188,6 +221,7 @@ module SolidQueue
|
|
|
188
221
|
if file.exist?
|
|
189
222
|
ActiveSupport::ConfigurationFile.parse(file).deep_symbolize_keys
|
|
190
223
|
else
|
|
224
|
+
puts "[solid_queue] WARNING: Provided configuration file '#{file}' does not exist. Falling back to default configuration."
|
|
191
225
|
{}
|
|
192
226
|
end
|
|
193
227
|
end
|
|
@@ -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
|
|
@@ -18,17 +18,19 @@ module SolidQueue::Processes
|
|
|
18
18
|
attr_accessor :process
|
|
19
19
|
|
|
20
20
|
def register
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
wrap_in_app_executor do
|
|
22
|
+
@process = SolidQueue::Process.register \
|
|
23
|
+
kind: kind,
|
|
24
|
+
name: name,
|
|
25
|
+
pid: pid,
|
|
26
|
+
hostname: hostname,
|
|
27
|
+
supervisor: try(:supervisor),
|
|
28
|
+
metadata: metadata.compact
|
|
29
|
+
end
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def deregister
|
|
31
|
-
process&.deregister
|
|
33
|
+
wrap_in_app_executor { process&.deregister }
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def registered?
|
|
@@ -52,10 +54,14 @@ module SolidQueue::Processes
|
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def heartbeat
|
|
55
|
-
process
|
|
57
|
+
process&.heartbeat
|
|
56
58
|
rescue ActiveRecord::RecordNotFound
|
|
57
59
|
self.process = nil
|
|
58
60
|
wake_up
|
|
59
61
|
end
|
|
62
|
+
|
|
63
|
+
def reload_metadata
|
|
64
|
+
wrap_in_app_executor { process&.update(metadata: metadata.compact) }
|
|
65
|
+
end
|
|
60
66
|
end
|
|
61
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
|