solid_queue 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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