solid_queue 0.2.2 → 0.3.1

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 (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