solid_queue 0.2.2 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -7
  3. data/app/models/solid_queue/blocked_execution.rb +16 -10
  4. data/app/models/solid_queue/claimed_execution.rb +11 -5
  5. data/app/models/solid_queue/execution/dispatching.rb +2 -3
  6. data/app/models/solid_queue/execution.rb +32 -15
  7. data/app/models/solid_queue/failed_execution.rb +10 -6
  8. data/app/models/solid_queue/job/clearable.rb +3 -3
  9. data/app/models/solid_queue/job/executable.rb +3 -7
  10. data/app/models/solid_queue/job/recurrable.rb +13 -0
  11. data/app/models/solid_queue/job/schedulable.rb +1 -1
  12. data/app/models/solid_queue/job.rb +1 -1
  13. data/app/models/solid_queue/process/prunable.rb +6 -5
  14. data/app/models/solid_queue/process.rb +13 -6
  15. data/app/models/solid_queue/recurring_execution.rb +26 -0
  16. data/app/models/solid_queue/scheduled_execution.rb +3 -1
  17. data/app/models/solid_queue/semaphore.rb +1 -1
  18. data/db/migrate/20240218110712_create_recurring_executions.rb +14 -0
  19. data/lib/active_job/queue_adapters/solid_queue_adapter.rb +4 -0
  20. data/lib/generators/solid_queue/install/templates/config.yml +1 -1
  21. data/lib/puma/plugin/solid_queue.rb +1 -0
  22. data/lib/solid_queue/app_executor.rb +1 -1
  23. data/lib/solid_queue/configuration.rb +14 -5
  24. data/lib/solid_queue/dispatcher/concurrency_maintenance.rb +44 -0
  25. data/lib/solid_queue/dispatcher/recurring_schedule.rb +56 -0
  26. data/lib/solid_queue/dispatcher/recurring_task.rb +91 -0
  27. data/lib/solid_queue/dispatcher.rb +24 -39
  28. data/lib/solid_queue/engine.rb +4 -2
  29. data/lib/solid_queue/log_subscriber.rb +164 -0
  30. data/lib/solid_queue/processes/base.rb +13 -14
  31. data/lib/solid_queue/processes/callbacks.rb +19 -0
  32. data/lib/solid_queue/processes/interruptible.rb +1 -1
  33. data/lib/solid_queue/processes/poller.rb +34 -4
  34. data/lib/solid_queue/processes/registrable.rb +9 -28
  35. data/lib/solid_queue/processes/runnable.rb +33 -47
  36. data/lib/solid_queue/processes/signals.rb +1 -1
  37. data/lib/solid_queue/processes/supervised.rb +4 -0
  38. data/lib/solid_queue/supervisor.rb +25 -24
  39. data/lib/solid_queue/version.rb +1 -1
  40. data/lib/solid_queue/worker.rb +15 -16
  41. data/lib/solid_queue.rb +27 -20
  42. metadata +129 -9
@@ -4,7 +4,7 @@ module SolidQueue
4
4
  class Configuration
5
5
  WORKER_DEFAULTS = {
6
6
  queues: "*",
7
- threads: 5,
7
+ threads: 3,
8
8
  processes: 1,
9
9
  polling_interval: 0.1
10
10
  }
@@ -12,7 +12,9 @@ module SolidQueue
12
12
  DISPATCHER_DEFAULTS = {
13
13
  batch_size: 500,
14
14
  polling_interval: 1,
15
- concurrency_maintenance_interval: 600
15
+ concurrency_maintenance: true,
16
+ concurrency_maintenance_interval: 600,
17
+ recurring_tasks: []
16
18
  }
17
19
 
18
20
  def initialize(mode: :work, load_from: nil)
@@ -33,7 +35,7 @@ module SolidQueue
33
35
  if mode.in? %i[ work all]
34
36
  workers_options.flat_map do |worker_options|
35
37
  processes = worker_options.fetch(:processes, WORKER_DEFAULTS[:processes])
36
- processes.times.collect { SolidQueue::Worker.new(**worker_options.with_defaults(WORKER_DEFAULTS)) }
38
+ processes.times.map { Worker.new(**worker_options.with_defaults(WORKER_DEFAULTS)) }
37
39
  end
38
40
  else
39
41
  []
@@ -42,8 +44,10 @@ module SolidQueue
42
44
 
43
45
  def dispatchers
44
46
  if mode.in? %i[ dispatch all]
45
- dispatchers_options.flat_map do |dispatcher_options|
46
- SolidQueue::Dispatcher.new(**dispatcher_options)
47
+ dispatchers_options.map do |dispatcher_options|
48
+ recurring_tasks = parse_recurring_tasks dispatcher_options[:recurring_tasks]
49
+
50
+ Dispatcher.new **dispatcher_options.merge(recurring_tasks: recurring_tasks).with_defaults(DISPATCHER_DEFAULTS)
47
51
  end
48
52
  end
49
53
  end
@@ -73,6 +77,11 @@ module SolidQueue
73
77
  .map { |options| options.dup.symbolize_keys }
74
78
  end
75
79
 
80
+ def parse_recurring_tasks(tasks)
81
+ Array(tasks).map do |id, options|
82
+ Dispatcher::RecurringTask.from_configuration(id, **options)
83
+ end.select(&:valid?)
84
+ end
76
85
 
77
86
  def load_config_from(file_or_hash)
78
87
  case file_or_hash
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Dispatcher::ConcurrencyMaintenance
5
+ include AppExecutor
6
+
7
+ attr_reader :interval, :batch_size
8
+
9
+ def initialize(interval, batch_size)
10
+ @interval = interval
11
+ @batch_size = batch_size
12
+ end
13
+
14
+ def start
15
+ @concurrency_maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: interval) do
16
+ expire_semaphores
17
+ unblock_blocked_executions
18
+ end
19
+
20
+ @concurrency_maintenance_task.add_observer do |_, _, error|
21
+ handle_thread_error(error) if error
22
+ end
23
+
24
+ @concurrency_maintenance_task.execute
25
+ end
26
+
27
+ def stop
28
+ @concurrency_maintenance_task.shutdown
29
+ end
30
+
31
+ private
32
+ def expire_semaphores
33
+ wrap_in_app_executor do
34
+ Semaphore.expired.in_batches(of: batch_size, &:delete_all)
35
+ end
36
+ end
37
+
38
+ def unblock_blocked_executions
39
+ wrap_in_app_executor do
40
+ BlockedExecution.unblock(batch_size)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Dispatcher::RecurringSchedule
5
+ include AppExecutor
6
+
7
+ attr_reader :configured_tasks, :scheduled_tasks
8
+
9
+ def initialize(tasks)
10
+ @configured_tasks = Array(tasks).map { |task| Dispatcher::RecurringTask.wrap(task) }
11
+ @scheduled_tasks = Concurrent::Hash.new
12
+ end
13
+
14
+ def load_tasks
15
+ configured_tasks.each do |task|
16
+ load_task(task)
17
+ end
18
+ end
19
+
20
+ def load_task(task)
21
+ scheduled_tasks[task.key] = schedule(task)
22
+ end
23
+
24
+ def unload_tasks
25
+ scheduled_tasks.values.each(&:cancel)
26
+ scheduled_tasks.clear
27
+ end
28
+
29
+ def tasks
30
+ configured_tasks.each_with_object({}) { |task, hsh| hsh[task.key] = task.to_h }
31
+ end
32
+
33
+ def inspect
34
+ configured_tasks.map(&:to_s).join(" | ")
35
+ end
36
+
37
+ private
38
+ def schedule(task)
39
+ scheduled_task = Concurrent::ScheduledTask.new(task.delay_from_now, args: [ self, task, task.next_time ]) do |thread_schedule, thread_task, thread_task_run_at|
40
+ thread_schedule.load_task(thread_task)
41
+
42
+ wrap_in_app_executor do
43
+ thread_task.enqueue(at: thread_task_run_at)
44
+ end
45
+ end
46
+
47
+ scheduled_task.add_observer do |_, _, error|
48
+ # Don't notify on task cancellation before execution, as this will happen normally
49
+ # as part of unloading tasks
50
+ handle_thread_error(error) if error && !error.is_a?(Concurrent::CancelledOperationError)
51
+ end
52
+
53
+ scheduled_task.tap(&:execute)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,91 @@
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
+ if using_solid_queue_adapter?
35
+ perform_later_and_record(run_at: at)
36
+ else
37
+ payload[:other_adapter] = true
38
+
39
+ perform_later
40
+ end.tap do |active_job|
41
+ payload[:active_job_id] = active_job&.job_id
42
+ end
43
+ end
44
+ end
45
+
46
+ def valid?
47
+ parsed_schedule.instance_of?(Fugit::Cron)
48
+ end
49
+
50
+ def to_s
51
+ "#{class_name}.perform_later(#{arguments.map(&:inspect).join(",")}) [ #{parsed_schedule.original} ]"
52
+ end
53
+
54
+ def to_h
55
+ {
56
+ schedule: schedule,
57
+ class_name: class_name,
58
+ arguments: arguments
59
+ }
60
+ end
61
+
62
+ private
63
+ def using_solid_queue_adapter?
64
+ job_class.queue_adapter_name.inquiry.solid_queue?
65
+ end
66
+
67
+ def perform_later_and_record(run_at:)
68
+ RecurringExecution.record(key, run_at) { perform_later }
69
+ end
70
+
71
+ def perform_later
72
+ job_class.perform_later(*arguments_with_kwargs)
73
+ end
74
+
75
+ def arguments_with_kwargs
76
+ if arguments.last.is_a?(Hash)
77
+ arguments[0...-1] + [ Hash.ruby2_keywords_hash(arguments.last) ]
78
+ else
79
+ arguments
80
+ end
81
+ end
82
+
83
+ def parsed_schedule
84
+ @parsed_schedule ||= Fugit.parse(schedule)
85
+ end
86
+
87
+ def job_class
88
+ @job_class ||= class_name.safe_constantize
89
+ end
90
+ end
91
+ end
@@ -2,72 +2,57 @@
2
2
 
3
3
  module SolidQueue
4
4
  class Dispatcher < Processes::Base
5
- include Processes::Runnable, Processes::Poller
5
+ include Processes::Poller
6
6
 
7
- attr_accessor :batch_size, :concurrency_maintenance_interval
7
+ attr_accessor :batch_size, :concurrency_maintenance, :recurring_schedule
8
8
 
9
- set_callback :boot, :after, :launch_concurrency_maintenance
10
- set_callback :shutdown, :before, :stop_concurrency_maintenance
9
+ after_boot :start_concurrency_maintenance, :load_recurring_schedule
10
+ before_shutdown :stop_concurrency_maintenance, :unload_recurring_schedule
11
11
 
12
12
  def initialize(**options)
13
13
  options = options.dup.with_defaults(SolidQueue::Configuration::DISPATCHER_DEFAULTS)
14
14
 
15
15
  @batch_size = options[:batch_size]
16
16
  @polling_interval = options[:polling_interval]
17
- @concurrency_maintenance_interval = options[:concurrency_maintenance_interval]
17
+
18
+ @concurrency_maintenance = ConcurrencyMaintenance.new(options[:concurrency_maintenance_interval], options[:batch_size]) if options[:concurrency_maintenance]
19
+ @recurring_schedule = RecurringSchedule.new(options[:recurring_tasks])
20
+ end
21
+
22
+ def metadata
23
+ super.merge(batch_size: batch_size, concurrency_maintenance_interval: concurrency_maintenance&.interval, recurring_schedule: recurring_schedule.tasks.presence)
18
24
  end
19
25
 
20
26
  private
21
- def run
27
+ def poll
22
28
  batch = dispatch_next_batch
23
-
24
- unless batch.size > 0
25
- procline "waiting"
26
- interruptible_sleep(polling_interval)
27
- end
29
+ batch.size
28
30
  end
29
31
 
30
32
  def dispatch_next_batch
31
33
  with_polling_volume do
32
- SolidQueue::ScheduledExecution.dispatch_next_batch(batch_size)
34
+ ScheduledExecution.dispatch_next_batch(batch_size)
33
35
  end
34
36
  end
35
37
 
36
- def launch_concurrency_maintenance
37
- @concurrency_maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: concurrency_maintenance_interval) do
38
- expire_semaphores
39
- unblock_blocked_executions
40
- end
41
-
42
- @concurrency_maintenance_task.add_observer do |_, _, error|
43
- handle_thread_error(error) if error
44
- end
45
-
46
- @concurrency_maintenance_task.execute
38
+ def start_concurrency_maintenance
39
+ concurrency_maintenance&.start
47
40
  end
48
41
 
49
- def stop_concurrency_maintenance
50
- @concurrency_maintenance_task.shutdown
42
+ def load_recurring_schedule
43
+ recurring_schedule.load_tasks
51
44
  end
52
45
 
53
- def expire_semaphores
54
- wrap_in_app_executor do
55
- Semaphore.expired.in_batches(of: batch_size, &:delete_all)
56
- end
57
- end
58
-
59
- def unblock_blocked_executions
60
- wrap_in_app_executor do
61
- BlockedExecution.unblock(batch_size)
62
- end
46
+ def stop_concurrency_maintenance
47
+ concurrency_maintenance&.stop
63
48
  end
64
49
 
65
- def initial_jitter
66
- Kernel.rand(0...polling_interval)
50
+ def unload_recurring_schedule
51
+ recurring_schedule.unload_tasks
67
52
  end
68
53
 
69
- def metadata
70
- super.merge(batch_size: batch_size)
54
+ def set_procline
55
+ procline "waiting"
71
56
  end
72
57
  end
73
58
  end
@@ -18,7 +18,7 @@ module SolidQueue
18
18
 
19
19
  initializer "solid_queue.app_executor", before: :run_prepare_callbacks do |app|
20
20
  config.solid_queue.app_executor ||= app.executor
21
- config.solid_queue.on_thread_error ||= -> (exception) { Rails.error.report(exception, handled: false) }
21
+ config.solid_queue.on_thread_error ||= ->(exception) { Rails.error.report(exception, handled: false) }
22
22
 
23
23
  SolidQueue.app_executor = config.solid_queue.app_executor
24
24
  SolidQueue.on_thread_error = config.solid_queue.on_thread_error
@@ -26,8 +26,10 @@ module SolidQueue
26
26
 
27
27
  initializer "solid_queue.logger" do |app|
28
28
  ActiveSupport.on_load(:solid_queue) do
29
- self.logger = app.logger
29
+ self.logger ||= app.logger
30
30
  end
31
+
32
+ SolidQueue::LogSubscriber.attach_to :solid_queue
31
33
  end
32
34
 
33
35
  initializer "solid_queue.active_job.extensions" do
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/log_subscriber"
4
+
5
+ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
6
+ def dispatch_scheduled(event)
7
+ debug formatted_event(event, action: "Dispatch scheduled jobs", **event.payload.slice(:batch_size, :size))
8
+ end
9
+
10
+ def release_many_claimed(event)
11
+ debug formatted_event(event, action: "Release claimed jobs", **event.payload.slice(:size))
12
+ end
13
+
14
+ def release_claimed(event)
15
+ debug formatted_event(event, action: "Release claimed job", **event.payload.slice(:job_id, :process_id))
16
+ end
17
+
18
+ def retry_all(event)
19
+ debug formatted_event(event, action: "Retry failed jobs", **event.payload.slice(:jobs_size, :size))
20
+ end
21
+
22
+ def retry(event)
23
+ debug formatted_event(event, action: "Retry failed job", **event.payload.slice(:job_id))
24
+ end
25
+
26
+ def discard_all(event)
27
+ debug formatted_event(event, action: "Discard jobs", **event.payload.slice(:jobs_size, :size, :status))
28
+ end
29
+
30
+ def discard(event)
31
+ debug formatted_event(event, action: "Discard job", **event.payload.slice(:job_id, :status))
32
+ end
33
+
34
+ def release_many_blocked(event)
35
+ debug formatted_event(event, action: "Unblock jobs", **event.payload.slice(:limit, :size))
36
+ end
37
+
38
+ def release_blocked(event)
39
+ debug formatted_event(event, action: "Release blocked job", **event.payload.slice(:job_id, :concurrency_key, :released))
40
+ end
41
+
42
+ def enqueue_recurring_task(event)
43
+ attributes = event.payload.slice(:task, :at, :active_job_id)
44
+
45
+ if event.payload[:other_adapter]
46
+ debug formatted_event(event, action: "Enqueued recurring task outside Solid Queue", **attributes)
47
+ else
48
+ action = attributes[:active_job_id].present? ? "Enqueued recurring task" : "Skipped recurring task – already dispatched"
49
+ info formatted_event(event, action: action, **attributes)
50
+ end
51
+ end
52
+
53
+ def start_process(event)
54
+ process = event.payload[:process]
55
+
56
+ attributes = {
57
+ pid: process.pid,
58
+ hostname: process.hostname
59
+ }.merge(process.metadata)
60
+
61
+ info formatted_event(event, action: "Started #{process.kind}", **attributes)
62
+ end
63
+
64
+ def shutdown_process(event)
65
+ process = event.payload[:process]
66
+
67
+ attributes = {
68
+ pid: process.pid,
69
+ hostname: process.hostname
70
+ }.merge(process.metadata)
71
+
72
+ info formatted_event(event, action: "Shut down #{process.kind}", **attributes)
73
+ end
74
+
75
+ def register_process(event)
76
+ process_kind = event.payload[:kind]
77
+ attributes = event.payload.slice(:pid, :hostname)
78
+
79
+ if error = event.payload[:error]
80
+ warn formatted_event(event, action: "Error registering #{process_kind}", **attributes.merge(error: formatted_error(error)))
81
+ else
82
+ info formatted_event(event, action: "Register #{process_kind}", **attributes)
83
+ end
84
+ end
85
+
86
+ def deregister_process(event)
87
+ process = event.payload[:process]
88
+
89
+ attributes = {
90
+ process_id: process.id,
91
+ pid: process.pid,
92
+ hostname: process.hostname,
93
+ last_heartbeat_at: process.last_heartbeat_at,
94
+ claimed_size: process.claimed_executions.size,
95
+ pruned: event.payload
96
+ }
97
+
98
+ if error = event.payload[:error]
99
+ warn formatted_event(event, action: "Error deregistering #{process.kind}", **attributes.merge(error: formatted_error(error)))
100
+ else
101
+ info formatted_event(event, action: "Deregister #{process.kind}", **attributes)
102
+ end
103
+ end
104
+
105
+ def prune_processes(event)
106
+ debug formatted_event(event, action: "Prune dead processes", **event.payload.slice(:size))
107
+ end
108
+
109
+ def thread_error(event)
110
+ error formatted_event(event, action: "Error in thread", error: formatted_error(event.payload[:error]))
111
+ end
112
+
113
+ def graceful_termination(event)
114
+ attributes = event.payload.slice(:supervisor_pid, :supervised_pids)
115
+
116
+ if event.payload[:shutdown_timeout_exceeded]
117
+ warn formatted_event(event, action: "Supervisor wasn't terminated gracefully - shutdown timeout exceeded", **attributes)
118
+ else
119
+ info formatted_event(event, action: "Supervisor terminated gracefully", **attributes)
120
+ end
121
+ end
122
+
123
+ def immediate_termination(event)
124
+ info formatted_event(event, action: "Supervisor terminated immediately", **event.payload.slice(:supervisor_pid, :supervised_pids))
125
+ end
126
+
127
+ def unhandled_signal_error(event)
128
+ error formatted_event(event, action: "Received unhandled signal", **event.payload.slice(:signal))
129
+ end
130
+
131
+ def replace_fork(event)
132
+ status = event.payload[:status]
133
+ attributes = event.payload.slice(:pid).merge \
134
+ status: (status.exitstatus || "no exit status set"),
135
+ pid_from_status: status.pid,
136
+ signaled: status.signaled?,
137
+ stopsig: status.stopsig,
138
+ termsig: status.termsig
139
+
140
+ if replaced_fork = event.payload[:fork]
141
+ info formatted_event(event, action: "Replaced terminated #{replaced_fork.kind}", **attributes.merge(hostname: replaced_fork.hostname))
142
+ else
143
+ warn formatted_event(event, action: "Tried to replace forked process but it had already died", **attributes)
144
+ end
145
+ end
146
+
147
+ private
148
+ def formatted_event(event, action:, **attributes)
149
+ "SolidQueue-#{SolidQueue::VERSION} #{action} (#{event.duration.round(1)}ms) #{formatted_attributes(**attributes)}"
150
+ end
151
+
152
+ def formatted_attributes(**attributes)
153
+ attributes.map { |attr, value| "#{attr}: #{value.inspect}" }.join(", ")
154
+ end
155
+
156
+ def formatted_error(error)
157
+ [ error.class, error.message ].compact.join(" ")
158
+ end
159
+
160
+ # Use the logger configured for SolidQueue
161
+ def logger
162
+ SolidQueue.logger
163
+ end
164
+ end
@@ -3,25 +3,24 @@
3
3
  module SolidQueue
4
4
  module Processes
5
5
  class Base
6
- include ActiveSupport::Callbacks
7
- define_callbacks :boot, :shutdown
8
-
6
+ include Callbacks # Defines callbacks needed by other concerns
9
7
  include AppExecutor, Registrable, Interruptible, Procline
10
8
 
11
- private
12
- def observe_initial_delay
13
- interruptible_sleep(initial_jitter)
14
- end
9
+ def kind
10
+ self.class.name.demodulize
11
+ end
15
12
 
16
- def boot
17
- end
13
+ def hostname
14
+ @hostname ||= Socket.gethostname.force_encoding(Encoding::UTF_8)
15
+ end
18
16
 
19
- def shutdown
20
- end
17
+ def pid
18
+ @pid ||= ::Process.pid
19
+ end
21
20
 
22
- def initial_jitter
23
- 0
24
- end
21
+ def metadata
22
+ {}
23
+ end
25
24
  end
26
25
  end
27
26
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue::Processes
4
+ module Callbacks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ extend ActiveModel::Callbacks
9
+ define_model_callbacks :boot, :shutdown
10
+ end
11
+
12
+ private
13
+ def boot
14
+ end
15
+
16
+ def shutdown
17
+ end
18
+ end
19
+ end
@@ -10,7 +10,7 @@ module SolidQueue::Processes
10
10
  SELF_PIPE_BLOCK_SIZE = 11
11
11
 
12
12
  def interrupt
13
- self_pipe[:writer].write_nonblock( "." )
13
+ self_pipe[:writer].write_nonblock(".")
14
14
  rescue Errno::EAGAIN, Errno::EINTR
15
15
  # Ignore writes that would block and retry
16
16
  # if another signal arrived while writing
@@ -4,11 +4,45 @@ module SolidQueue::Processes
4
4
  module Poller
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ include Runnable
8
+
7
9
  included do
8
10
  attr_accessor :polling_interval
9
11
  end
10
12
 
13
+ def metadata
14
+ super.merge(polling_interval: polling_interval)
15
+ end
16
+
11
17
  private
18
+ def run
19
+ if mode.async?
20
+ @thread = Thread.new { start_loop }
21
+ else
22
+ start_loop
23
+ end
24
+ end
25
+
26
+ def start_loop
27
+ loop do
28
+ break if shutting_down?
29
+
30
+ wrap_in_app_executor do
31
+ unless poll > 0
32
+ interruptible_sleep(polling_interval)
33
+ end
34
+ end
35
+ end
36
+ ensure
37
+ SolidQueue.instrument(:shutdown_process, process: self) do
38
+ run_callbacks(:shutdown) { shutdown }
39
+ end
40
+ end
41
+
42
+ def poll
43
+ raise NotImplementedError
44
+ end
45
+
12
46
  def with_polling_volume
13
47
  if SolidQueue.silence_polling?
14
48
  ActiveRecord::Base.logger.silence { yield }
@@ -16,9 +50,5 @@ module SolidQueue::Processes
16
50
  yield
17
51
  end
18
52
  end
19
-
20
- def metadata
21
- super.merge(polling_interval: polling_interval)
22
- end
23
53
  end
24
54
  end