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