solid_queue 0.3.3 → 0.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -14
  3. data/app/models/solid_queue/claimed_execution.rb +10 -3
  4. data/app/models/solid_queue/failed_execution.rb +37 -1
  5. data/app/models/solid_queue/job.rb +7 -0
  6. data/app/models/solid_queue/process/executor.rb +20 -0
  7. data/app/models/solid_queue/process/prunable.rb +15 -11
  8. data/app/models/solid_queue/process.rb +10 -9
  9. data/app/models/solid_queue/recurring_execution.rb +7 -3
  10. data/lib/puma/plugin/solid_queue.rb +39 -11
  11. data/lib/solid_queue/configuration.rb +18 -22
  12. data/lib/solid_queue/dispatcher/concurrency_maintenance.rb +1 -1
  13. data/lib/solid_queue/dispatcher/recurring_schedule.rb +4 -0
  14. data/lib/solid_queue/dispatcher/recurring_task.rb +14 -6
  15. data/lib/solid_queue/dispatcher.rb +7 -4
  16. data/lib/solid_queue/log_subscriber.rb +22 -13
  17. data/lib/solid_queue/processes/callbacks.rb +0 -7
  18. data/lib/solid_queue/processes/poller.rb +10 -11
  19. data/lib/solid_queue/processes/procline.rb +1 -1
  20. data/lib/solid_queue/processes/registrable.rb +9 -1
  21. data/lib/solid_queue/processes/runnable.rb +38 -7
  22. data/lib/solid_queue/processes/supervised.rb +2 -3
  23. data/lib/solid_queue/supervisor/async_supervisor.rb +44 -0
  24. data/lib/solid_queue/supervisor/fork_supervisor.rb +108 -0
  25. data/lib/solid_queue/supervisor/maintenance.rb +34 -0
  26. data/lib/solid_queue/{processes → supervisor}/pidfile.rb +2 -2
  27. data/lib/solid_queue/supervisor/pidfiled.rb +25 -0
  28. data/lib/solid_queue/supervisor/signals.rb +67 -0
  29. data/lib/solid_queue/supervisor.rb +32 -142
  30. data/lib/solid_queue/tasks.rb +1 -11
  31. data/lib/solid_queue/timer.rb +28 -0
  32. data/lib/solid_queue/version.rb +1 -1
  33. data/lib/solid_queue/worker.rb +4 -5
  34. metadata +13 -5
  35. data/lib/solid_queue/processes/signals.rb +0 -69
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueue
4
- class Dispatcher < Processes::Base
5
- include Processes::Poller
6
-
4
+ class Dispatcher < Processes::Poller
7
5
  attr_accessor :batch_size, :concurrency_maintenance, :recurring_schedule
8
6
 
9
7
  after_boot :start_concurrency_maintenance, :load_recurring_schedule
@@ -13,10 +11,11 @@ module SolidQueue
13
11
  options = options.dup.with_defaults(SolidQueue::Configuration::DISPATCHER_DEFAULTS)
14
12
 
15
13
  @batch_size = options[:batch_size]
16
- @polling_interval = options[:polling_interval]
17
14
 
18
15
  @concurrency_maintenance = ConcurrencyMaintenance.new(options[:concurrency_maintenance_interval], options[:batch_size]) if options[:concurrency_maintenance]
19
16
  @recurring_schedule = RecurringSchedule.new(options[:recurring_tasks])
17
+
18
+ super(**options)
20
19
  end
21
20
 
22
21
  def metadata
@@ -51,6 +50,10 @@ module SolidQueue
51
50
  recurring_schedule.unload_tasks
52
51
  end
53
52
 
53
+ def all_work_completed?
54
+ SolidQueue::ScheduledExecution.none? && recurring_schedule.empty?
55
+ end
56
+
54
57
  def set_procline
55
58
  procline "waiting"
56
59
  end
@@ -7,6 +7,10 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
7
7
  debug formatted_event(event, action: "Dispatch scheduled jobs", **event.payload.slice(:batch_size, :size))
8
8
  end
9
9
 
10
+ def claim(event)
11
+ debug formatted_event(event, action: "Claim jobs", **event.payload.slice(:process_id, :job_ids, :claimed_job_ids, :size))
12
+ end
13
+
10
14
  def release_many_claimed(event)
11
15
  debug formatted_event(event, action: "Release claimed jobs", **event.payload.slice(:size))
12
16
  end
@@ -40,13 +44,16 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
40
44
  end
41
45
 
42
46
  def enqueue_recurring_task(event)
43
- attributes = event.payload.slice(:task, :at, :active_job_id)
47
+ attributes = event.payload.slice(:task, :active_job_id, :enqueue_error)
48
+ attributes[:at] = event.payload[:at]&.iso8601
44
49
 
45
- if event.payload[:other_adapter]
50
+ if attributes[:active_job_id].nil? && event.payload[:skipped].nil?
51
+ error formatted_event(event, action: "Error enqueuing recurring task", **attributes)
52
+ elsif event.payload[:other_adapter]
46
53
  debug formatted_event(event, action: "Enqueued recurring task outside Solid Queue", **attributes)
47
54
  else
48
- action = attributes[:active_job_id].present? ? "Enqueued recurring task" : "Skipped recurring task – already dispatched"
49
- info formatted_event(event, action: action, **attributes)
55
+ action = event.payload[:skipped].present? ? "Skipped recurring task – already dispatched" : "Enqueued recurring task"
56
+ debug formatted_event(event, action: action, **attributes)
50
57
  end
51
58
  end
52
59
 
@@ -55,7 +62,8 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
55
62
 
56
63
  attributes = {
57
64
  pid: process.pid,
58
- hostname: process.hostname
65
+ hostname: process.hostname,
66
+ process_id: process.process_id
59
67
  }.merge(process.metadata)
60
68
 
61
69
  info formatted_event(event, action: "Started #{process.kind}", **attributes)
@@ -66,15 +74,16 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
66
74
 
67
75
  attributes = {
68
76
  pid: process.pid,
69
- hostname: process.hostname
77
+ hostname: process.hostname,
78
+ process_id: process.process_id
70
79
  }.merge(process.metadata)
71
80
 
72
- info formatted_event(event, action: "Shut down #{process.kind}", **attributes)
81
+ info formatted_event(event, action: "Shutdown #{process.kind}", **attributes)
73
82
  end
74
83
 
75
84
  def register_process(event)
76
85
  process_kind = event.payload[:kind]
77
- attributes = event.payload.slice(:pid, :hostname)
86
+ attributes = event.payload.slice(:pid, :hostname, :process_id)
78
87
 
79
88
  if error = event.payload[:error]
80
89
  warn formatted_event(event, action: "Error registering #{process_kind}", **attributes.merge(error: formatted_error(error)))
@@ -90,9 +99,9 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
90
99
  process_id: process.id,
91
100
  pid: process.pid,
92
101
  hostname: process.hostname,
93
- last_heartbeat_at: process.last_heartbeat_at,
94
- claimed_size: process.claimed_executions.size,
95
- pruned: event.payload
102
+ last_heartbeat_at: process.last_heartbeat_at.iso8601,
103
+ claimed_size: event.payload[:claimed_size],
104
+ pruned: event.payload[:pruned]
96
105
  }
97
106
 
98
107
  if error = event.payload[:error]
@@ -111,7 +120,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
111
120
  end
112
121
 
113
122
  def graceful_termination(event)
114
- attributes = event.payload.slice(:supervisor_pid, :supervised_pids)
123
+ attributes = event.payload.slice(:process_id, :supervisor_pid, :supervised_processes)
115
124
 
116
125
  if event.payload[:shutdown_timeout_exceeded]
117
126
  warn formatted_event(event, action: "Supervisor wasn't terminated gracefully - shutdown timeout exceeded", **attributes)
@@ -121,7 +130,7 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
121
130
  end
122
131
 
123
132
  def immediate_termination(event)
124
- info formatted_event(event, action: "Supervisor terminated immediately", **event.payload.slice(:supervisor_pid, :supervised_pids))
133
+ info formatted_event(event, action: "Supervisor terminated immediately", **event.payload.slice(:process_id, :supervisor_pid, :supervised_processes))
125
134
  end
126
135
 
127
136
  def unhandled_signal_error(event)
@@ -8,12 +8,5 @@ module SolidQueue::Processes
8
8
  extend ActiveModel::Callbacks
9
9
  define_model_callbacks :boot, :shutdown
10
10
  end
11
-
12
- private
13
- def boot
14
- end
15
-
16
- def shutdown
17
- end
18
11
  end
19
12
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueue::Processes
4
- module Poller
5
- extend ActiveSupport::Concern
6
-
4
+ class Poller < Base
7
5
  include Runnable
8
6
 
9
- included do
10
- attr_accessor :polling_interval
7
+ attr_accessor :polling_interval
8
+
9
+ def initialize(polling_interval:, **options)
10
+ @polling_interval = polling_interval
11
11
  end
12
12
 
13
13
  def metadata
@@ -16,11 +16,7 @@ module SolidQueue::Processes
16
16
 
17
17
  private
18
18
  def run
19
- if mode.async?
20
- @thread = Thread.new { start_loop }
21
- else
22
- start_loop
23
- end
19
+ start_loop
24
20
  end
25
21
 
26
22
  def start_loop
@@ -43,8 +39,11 @@ module SolidQueue::Processes
43
39
  raise NotImplementedError
44
40
  end
45
41
 
42
+ def shutdown
43
+ end
44
+
46
45
  def with_polling_volume
47
- if SolidQueue.silence_polling?
46
+ if SolidQueue.silence_polling? && ActiveRecord::Base.logger
48
47
  ActiveRecord::Base.logger.silence { yield }
49
48
  else
50
49
  yield
@@ -5,7 +5,7 @@ module SolidQueue::Processes
5
5
  # Sets the procline ($0)
6
6
  # solid-queue-supervisor(0.1.0): <string>
7
7
  def procline(string)
8
- $0 = "solid-queue-#{self.class.name.demodulize.downcase}(#{SolidQueue::VERSION}): #{string}"
8
+ $0 = "solid-queue-#{self.class.name.demodulize.underscore.dasherize}(#{SolidQueue::VERSION}): #{string}"
9
9
  end
10
10
  end
11
11
  end
@@ -11,12 +11,16 @@ module SolidQueue::Processes
11
11
  after_shutdown :deregister
12
12
  end
13
13
 
14
+ def process_id
15
+ process&.id
16
+ end
17
+
14
18
  private
15
19
  attr_accessor :process
16
20
 
17
21
  def register
18
22
  @process = SolidQueue::Process.register \
19
- kind: self.class.name.demodulize,
23
+ kind: kind,
20
24
  pid: pid,
21
25
  hostname: hostname,
22
26
  supervisor: try(:supervisor),
@@ -36,6 +40,10 @@ module SolidQueue::Processes
36
40
  wrap_in_app_executor { heartbeat }
37
41
  end
38
42
 
43
+ @heartbeat_task.add_observer do |_, _, error|
44
+ handle_thread_error(error) if error
45
+ end
46
+
39
47
  @heartbeat_task.execute
40
48
  end
41
49
 
@@ -7,20 +7,32 @@ module SolidQueue::Processes
7
7
  attr_writer :mode
8
8
 
9
9
  def start
10
- @stopping = false
10
+ @stopped = false
11
11
 
12
12
  SolidQueue.instrument(:start_process, process: self) do
13
13
  run_callbacks(:boot) { boot }
14
14
  end
15
15
 
16
- run
16
+ if running_async?
17
+ @thread = create_thread { run }
18
+ else
19
+ run
20
+ end
17
21
  end
18
22
 
19
23
  def stop
20
- @stopping = true
24
+ @stopped = true
21
25
  @thread&.join
22
26
  end
23
27
 
28
+ def name
29
+ @name ||= [ kind.downcase, SecureRandom.hex(6) ].join("-")
30
+ end
31
+
32
+ def alive?
33
+ !running_async? || @thread.alive?
34
+ end
35
+
24
36
  private
25
37
  DEFAULT_MODE = :async
26
38
 
@@ -29,22 +41,22 @@ module SolidQueue::Processes
29
41
  end
30
42
 
31
43
  def boot
32
- if supervised?
44
+ if running_as_fork?
33
45
  register_signal_handlers
34
46
  set_procline
35
47
  end
36
48
  end
37
49
 
38
50
  def shutting_down?
39
- stopping? || supervisor_went_away? || finished?
51
+ stopped? || (running_as_fork? && supervisor_went_away?) || finished?
40
52
  end
41
53
 
42
54
  def run
43
55
  raise NotImplementedError
44
56
  end
45
57
 
46
- def stopping?
47
- @stopping
58
+ def stopped?
59
+ @stopped
48
60
  end
49
61
 
50
62
  def finished?
@@ -61,5 +73,24 @@ module SolidQueue::Processes
61
73
  def running_inline?
62
74
  mode.inline?
63
75
  end
76
+
77
+ def running_async?
78
+ mode.async?
79
+ end
80
+
81
+ def running_as_fork?
82
+ mode.fork?
83
+ end
84
+
85
+
86
+ def create_thread(&block)
87
+ Thread.new do
88
+ Thread.current.name = name
89
+ block.call
90
+ rescue Exception => exception
91
+ handle_thread_error(exception)
92
+ raise
93
+ end
94
+ end
64
95
  end
65
96
  end
@@ -9,7 +9,6 @@ module SolidQueue::Processes
9
9
  end
10
10
 
11
11
  def supervised_by(process)
12
- self.mode = :supervised
13
12
  @supervisor = process
14
13
  end
15
14
 
@@ -19,11 +18,11 @@ module SolidQueue::Processes
19
18
  end
20
19
 
21
20
  def supervisor_went_away?
22
- supervised? && supervisor&.pid != ::Process.ppid
21
+ supervised? && supervisor.pid != ::Process.ppid
23
22
  end
24
23
 
25
24
  def supervised?
26
- mode.supervised?
25
+ supervisor.present?
27
26
  end
28
27
 
29
28
  def register_signal_handlers
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Supervisor::AsyncSupervisor < Supervisor
5
+ def initialize(*)
6
+ super
7
+ @threads = Concurrent::Map.new
8
+ end
9
+
10
+ def kind
11
+ "Supervisor(async)"
12
+ end
13
+
14
+ def stop
15
+ super
16
+ stop_threads
17
+ threads.clear
18
+
19
+ shutdown
20
+ end
21
+
22
+ private
23
+ attr_reader :threads
24
+
25
+ def start_process(configured_process)
26
+ configured_process.supervised_by process
27
+ configured_process.start
28
+
29
+ threads[configured_process.name] = configured_process
30
+ end
31
+
32
+ def stop_threads
33
+ stop_threads = threads.values.map do |thr|
34
+ Thread.new { thr.stop }
35
+ end
36
+
37
+ stop_threads.each { |thr| thr.join(SolidQueue.shutdown_timeout) }
38
+ end
39
+
40
+ def all_threads_terminated?
41
+ threads.values.none?(&:alive?)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Supervisor::ForkSupervisor < Supervisor
5
+ include Signals, Pidfiled
6
+
7
+ def initialize(*)
8
+ super
9
+ @forks = {}
10
+ end
11
+
12
+ def kind
13
+ "Supervisor(fork)"
14
+ end
15
+
16
+ private
17
+ attr_reader :forks
18
+
19
+ def supervise
20
+ loop do
21
+ break if stopped?
22
+
23
+ procline "supervising #{forks.keys.join(", ")}"
24
+ process_signal_queue
25
+
26
+ unless stopped?
27
+ reap_and_replace_terminated_forks
28
+ interruptible_sleep(1.second)
29
+ end
30
+ end
31
+ ensure
32
+ shutdown
33
+ end
34
+
35
+ def start_process(configured_process)
36
+ configured_process.supervised_by process
37
+ configured_process.mode = :fork
38
+
39
+ pid = fork do
40
+ configured_process.start
41
+ end
42
+
43
+ forks[pid] = configured_process
44
+ end
45
+
46
+ def terminate_gracefully
47
+ SolidQueue.instrument(:graceful_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do |payload|
48
+ term_forks
49
+
50
+ Timer.wait_until(SolidQueue.shutdown_timeout, -> { all_forks_terminated? }) do
51
+ reap_terminated_forks
52
+ end
53
+
54
+ unless all_forks_terminated?
55
+ payload[:shutdown_timeout_exceeded] = true
56
+ terminate_immediately
57
+ end
58
+ end
59
+ end
60
+
61
+ def terminate_immediately
62
+ SolidQueue.instrument(:immediate_termination, process_id: process_id, supervisor_pid: ::Process.pid, supervised_processes: forks.keys) do
63
+ quit_forks
64
+ end
65
+ end
66
+
67
+ def term_forks
68
+ signal_processes(forks.keys, :TERM)
69
+ end
70
+
71
+ def quit_forks
72
+ signal_processes(forks.keys, :QUIT)
73
+ end
74
+
75
+ def reap_and_replace_terminated_forks
76
+ loop do
77
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
78
+ break unless pid
79
+
80
+ replace_fork(pid, status)
81
+ end
82
+ end
83
+
84
+ def reap_terminated_forks
85
+ loop do
86
+ pid, status = ::Process.waitpid2(-1, ::Process::WNOHANG)
87
+ break unless pid
88
+
89
+ forks.delete(pid)
90
+ end
91
+ rescue SystemCallError
92
+ # All children already reaped
93
+ end
94
+
95
+ def replace_fork(pid, status)
96
+ SolidQueue.instrument(:replace_fork, supervisor_pid: ::Process.pid, pid: pid, status: status) do |payload|
97
+ if supervised_fork = forks.delete(pid)
98
+ payload[:fork] = supervised_fork
99
+ start_process(supervised_fork)
100
+ end
101
+ end
102
+ end
103
+
104
+ def all_forks_terminated?
105
+ forks.empty?
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,34 @@
1
+ module SolidQueue
2
+ module Supervisor::Maintenance
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ after_boot :release_orphaned_executions
7
+ end
8
+
9
+ private
10
+ def launch_maintenance_task
11
+ @maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: SolidQueue.process_alive_threshold) do
12
+ prune_dead_processes
13
+ end
14
+
15
+ @maintenance_task.add_observer do |_, _, error|
16
+ handle_thread_error(error) if error
17
+ end
18
+
19
+ @maintenance_task.execute
20
+ end
21
+
22
+ def stop_maintenance_task
23
+ @maintenance_task&.shutdown
24
+ end
25
+
26
+ def prune_dead_processes
27
+ wrap_in_app_executor { SolidQueue::Process.prune }
28
+ end
29
+
30
+ def release_orphaned_executions
31
+ wrap_in_app_executor { SolidQueue::ClaimedExecution.orphaned.release_all }
32
+ end
33
+ end
34
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SolidQueue::Processes
4
- class Pidfile
3
+ module SolidQueue
4
+ class Supervisor::Pidfile
5
5
  def initialize(path)
6
6
  @path = path
7
7
  @pid = ::Process.pid
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Supervisor
5
+ module Pidfiled
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_boot :setup_pidfile
10
+ after_shutdown :delete_pidfile
11
+ end
12
+
13
+ private
14
+ def setup_pidfile
15
+ if path = SolidQueue.supervisor_pidfile
16
+ @pidfile = Pidfile.new(path).tap(&:setup)
17
+ end
18
+ end
19
+
20
+ def delete_pidfile
21
+ @pidfile&.delete
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Supervisor
5
+ module Signals
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ before_boot :register_signal_handlers
10
+ after_shutdown :restore_default_signal_handlers
11
+ end
12
+
13
+ private
14
+ SIGNALS = %i[ QUIT INT TERM ]
15
+
16
+ def register_signal_handlers
17
+ SIGNALS.each do |signal|
18
+ trap(signal) do
19
+ signal_queue << signal
20
+ interrupt
21
+ end
22
+ end
23
+ end
24
+
25
+ def restore_default_signal_handlers
26
+ SIGNALS.each do |signal|
27
+ trap(signal, :DEFAULT)
28
+ end
29
+ end
30
+
31
+ def process_signal_queue
32
+ while signal = signal_queue.shift
33
+ handle_signal(signal)
34
+ end
35
+ end
36
+
37
+ def handle_signal(signal)
38
+ case signal
39
+ when :TERM, :INT
40
+ stop
41
+ terminate_gracefully
42
+ when :QUIT
43
+ stop
44
+ terminate_immediately
45
+ else
46
+ SolidQueue.instrument :unhandled_signal_error, signal: signal
47
+ end
48
+ end
49
+
50
+ def signal_processes(pids, signal)
51
+ pids.each do |pid|
52
+ signal_process pid, signal
53
+ end
54
+ end
55
+
56
+ def signal_process(pid, signal)
57
+ ::Process.kill signal, pid
58
+ rescue Errno::ESRCH
59
+ # Ignore, process died before
60
+ end
61
+
62
+ def signal_queue
63
+ @signal_queue ||= []
64
+ end
65
+ end
66
+ end
67
+ end