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.
- checksums.yaml +4 -4
- data/README.md +60 -7
- data/app/models/solid_queue/blocked_execution.rb +16 -10
- data/app/models/solid_queue/claimed_execution.rb +11 -5
- data/app/models/solid_queue/execution/dispatching.rb +2 -3
- data/app/models/solid_queue/execution.rb +32 -15
- data/app/models/solid_queue/failed_execution.rb +10 -6
- data/app/models/solid_queue/job/clearable.rb +3 -3
- data/app/models/solid_queue/job/executable.rb +3 -7
- data/app/models/solid_queue/job/recurrable.rb +13 -0
- data/app/models/solid_queue/job/schedulable.rb +1 -1
- data/app/models/solid_queue/job.rb +1 -1
- data/app/models/solid_queue/process/prunable.rb +6 -5
- data/app/models/solid_queue/process.rb +13 -6
- data/app/models/solid_queue/recurring_execution.rb +26 -0
- data/app/models/solid_queue/scheduled_execution.rb +3 -1
- data/app/models/solid_queue/semaphore.rb +1 -1
- data/db/migrate/20240218110712_create_recurring_executions.rb +14 -0
- data/lib/active_job/queue_adapters/solid_queue_adapter.rb +4 -0
- data/lib/generators/solid_queue/install/templates/config.yml +1 -1
- data/lib/puma/plugin/solid_queue.rb +1 -0
- data/lib/solid_queue/app_executor.rb +1 -1
- data/lib/solid_queue/configuration.rb +14 -5
- data/lib/solid_queue/dispatcher/concurrency_maintenance.rb +44 -0
- data/lib/solid_queue/dispatcher/recurring_schedule.rb +56 -0
- data/lib/solid_queue/dispatcher/recurring_task.rb +91 -0
- data/lib/solid_queue/dispatcher.rb +24 -39
- data/lib/solid_queue/engine.rb +4 -2
- data/lib/solid_queue/log_subscriber.rb +164 -0
- data/lib/solid_queue/processes/base.rb +13 -14
- data/lib/solid_queue/processes/callbacks.rb +19 -0
- data/lib/solid_queue/processes/interruptible.rb +1 -1
- data/lib/solid_queue/processes/poller.rb +34 -4
- data/lib/solid_queue/processes/registrable.rb +9 -28
- data/lib/solid_queue/processes/runnable.rb +33 -47
- data/lib/solid_queue/processes/signals.rb +1 -1
- data/lib/solid_queue/processes/supervised.rb +4 -0
- data/lib/solid_queue/supervisor.rb +25 -24
- data/lib/solid_queue/version.rb +1 -1
- data/lib/solid_queue/worker.rb +15 -16
- data/lib/solid_queue.rb +27 -20
- metadata +129 -9
@@ -4,7 +4,7 @@ module SolidQueue
|
|
4
4
|
class Configuration
|
5
5
|
WORKER_DEFAULTS = {
|
6
6
|
queues: "*",
|
7
|
-
threads:
|
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
|
-
|
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.
|
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.
|
46
|
-
|
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::
|
5
|
+
include Processes::Poller
|
6
6
|
|
7
|
-
attr_accessor :batch_size, :
|
7
|
+
attr_accessor :batch_size, :concurrency_maintenance, :recurring_schedule
|
8
8
|
|
9
|
-
|
10
|
-
|
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
|
-
|
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
|
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
|
-
|
34
|
+
ScheduledExecution.dispatch_next_batch(batch_size)
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
36
|
-
def
|
37
|
-
|
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
|
50
|
-
|
42
|
+
def load_recurring_schedule
|
43
|
+
recurring_schedule.load_tasks
|
51
44
|
end
|
52
45
|
|
53
|
-
def
|
54
|
-
|
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
|
66
|
-
|
50
|
+
def unload_recurring_schedule
|
51
|
+
recurring_schedule.unload_tasks
|
67
52
|
end
|
68
53
|
|
69
|
-
def
|
70
|
-
|
54
|
+
def set_procline
|
55
|
+
procline "waiting"
|
71
56
|
end
|
72
57
|
end
|
73
58
|
end
|
data/lib/solid_queue/engine.rb
CHANGED
@@ -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 ||= ->
|
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
|
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
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
9
|
+
def kind
|
10
|
+
self.class.name.demodulize
|
11
|
+
end
|
15
12
|
|
16
|
-
|
17
|
-
|
13
|
+
def hostname
|
14
|
+
@hostname ||= Socket.gethostname.force_encoding(Encoding::UTF_8)
|
15
|
+
end
|
18
16
|
|
19
|
-
|
20
|
-
|
17
|
+
def pid
|
18
|
+
@pid ||= ::Process.pid
|
19
|
+
end
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|