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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -32
  3. data/app/models/solid_queue/blocked_execution.rb +3 -1
  4. data/app/models/solid_queue/claimed_execution.rb +4 -2
  5. data/app/models/solid_queue/failed_execution.rb +6 -3
  6. data/app/models/solid_queue/job/concurrency_controls.rb +1 -1
  7. data/app/models/solid_queue/job/retryable.rb +10 -1
  8. data/app/models/solid_queue/job.rb +1 -0
  9. data/app/models/solid_queue/ready_execution.rb +2 -1
  10. data/app/models/solid_queue/record.rb +20 -5
  11. data/app/models/solid_queue/recurring_execution.rb +1 -1
  12. data/app/models/solid_queue/recurring_task.rb +27 -9
  13. data/app/models/solid_queue/semaphore.rb +2 -2
  14. data/lib/puma/plugin/solid_queue.rb +74 -14
  15. data/lib/solid_queue/app_executor.rb +10 -0
  16. data/lib/solid_queue/async_supervisor.rb +52 -0
  17. data/lib/solid_queue/cli.rb +5 -1
  18. data/lib/solid_queue/configuration.rb +42 -8
  19. data/lib/solid_queue/dispatcher.rb +1 -0
  20. data/lib/solid_queue/fork_supervisor.rb +68 -0
  21. data/lib/solid_queue/processes/registrable.rb +15 -9
  22. data/lib/solid_queue/processes/runnable.rb +25 -18
  23. data/lib/solid_queue/processes/thread_terminated_error.rb +11 -0
  24. data/lib/solid_queue/scheduler/recurring_schedule.rb +61 -11
  25. data/lib/solid_queue/scheduler.rb +23 -4
  26. data/lib/solid_queue/supervisor/maintenance.rb +11 -0
  27. data/lib/solid_queue/supervisor/signals.rb +2 -2
  28. data/lib/solid_queue/supervisor.rb +40 -82
  29. data/lib/solid_queue/timer.rb +3 -3
  30. data/lib/solid_queue/version.rb +1 -1
  31. data/lib/solid_queue.rb +8 -0
  32. metadata +25 -9
  33. 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
- in_background do
11
- monitor_solid_queue
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
- launcher.events.on_booted do
15
- @solid_queue_pid = fork do
16
- Thread.new { monitor_puma }
17
- SolidQueue::Supervisor.start
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.events.on_stopped { stop_solid_queue }
22
- launcher.events.on_restart { stop_solid_queue }
23
- end
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) if 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(:solid_queue_dead?, "Detected Solid Queue has gone away, stopping Puma...")
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 solid_queue_dead?
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
@@ -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, default: false,
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 = worker_options.fetch(:processes, WORKER_DEFAULTS[: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 !skip_recurring_tasks? && recurring_tasks.any?
126
- [ Process.new(:scheduler, recurring_tasks: recurring_tasks) ]
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: { workers: [ WORKER_DEFAULTS ], dispatchers: [ DISPATCHER_DEFAULTS ] }
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
@@ -3,6 +3,7 @@
3
3
  module SolidQueue
4
4
  class Dispatcher < Processes::Poller
5
5
  include LifecycleHooks
6
+
6
7
  attr_reader :batch_size
7
8
 
8
9
  after_boot :run_start_hooks
@@ -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
- @process = SolidQueue::Process.register \
22
- kind: kind,
23
- name: name,
24
- pid: pid,
25
- hostname: hostname,
26
- supervisor: try(:supervisor),
27
- metadata: metadata.compact
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.heartbeat
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
- boot
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
- @thread&.join
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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ module Processes
5
+ class ThreadTerminatedError < RuntimeError
6
+ def initialize(name)
7
+ super("Thread #{name} terminated unexpectedly")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -4,21 +4,28 @@ module SolidQueue
4
4
  class Scheduler::RecurringSchedule
5
5
  include AppExecutor
6
6
 
7
- attr_reader :configured_tasks, :scheduled_tasks
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
- configured_tasks.empty?
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
- persist_tasks
21
- reload_tasks
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
- def persist_tasks
44
- SolidQueue::RecurringTask.static.where.not(key: task_keys).delete_all
45
- SolidQueue::RecurringTask.create_or_update_all configured_tasks
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 reload_tasks
49
- @configured_tasks = SolidQueue::RecurringTask.where(key: task_keys)
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
- @recurring_schedule = RecurringSchedule.new(recurring_tasks)
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
- SLEEP_INTERVAL = 60 # Right now it doesn't matter, can be set to 1 in the future for dynamic tasks
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
- interruptible_sleep(SLEEP_INTERVAL)
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