solid_queue 1.6.0 → 1.7.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.
- checksums.yaml +4 -4
- data/README.md +133 -1
- data/UPGRADING.md +12 -0
- data/app/models/solid_queue/batch/callbacks.rb +50 -0
- data/app/models/solid_queue/batch/clearable.rb +23 -0
- data/app/models/solid_queue/batch/status.rb +64 -0
- data/app/models/solid_queue/batch/sweepable.rb +64 -0
- data/app/models/solid_queue/batch.rb +133 -0
- data/app/models/solid_queue/batch_execution.rb +52 -0
- data/app/models/solid_queue/claimed_execution.rb +1 -0
- data/app/models/solid_queue/failed_execution/batchable.rb +22 -0
- data/app/models/solid_queue/failed_execution.rb +1 -1
- data/app/models/solid_queue/job/batchable.rb +50 -0
- data/app/models/solid_queue/job/executable.rb +5 -1
- data/app/models/solid_queue/job.rb +11 -3
- data/lib/active_job/batch_id.rb +57 -0
- data/lib/generators/solid_queue/install/templates/db/queue_schema.rb +31 -0
- data/lib/generators/solid_queue/update/templates/db/add_batches_to_solid_queue.rb +39 -0
- data/lib/solid_queue/configuration.rb +2 -1
- data/lib/solid_queue/dispatcher/concurrency_maintenance.rb +4 -37
- data/lib/solid_queue/dispatcher/maintenance.rb +79 -0
- data/lib/solid_queue/dispatcher.rb +13 -9
- data/lib/solid_queue/engine.rb +4 -0
- data/lib/solid_queue/fork_supervisor.rb +13 -4
- data/lib/solid_queue/log_subscriber.rb +16 -1
- data/lib/solid_queue/processes/runnable.rb +2 -5
- data/lib/solid_queue/processes/supervised.rb +7 -0
- data/lib/solid_queue/supervisor/signals.rb +3 -0
- data/lib/solid_queue/supervisor.rb +29 -16
- data/lib/solid_queue/version.rb +1 -1
- data/lib/solid_queue.rb +1 -0
- metadata +13 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class Job
|
|
5
|
+
module Batchable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
belongs_to :batch, optional: true
|
|
10
|
+
has_one :batch_execution
|
|
11
|
+
|
|
12
|
+
after_create :create_batch_execution, if: :batched?
|
|
13
|
+
after_update :update_batch_progress, if: :batched?
|
|
14
|
+
before_destroy :destroy_batch_execution, if: :batched?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class_methods do
|
|
18
|
+
def batch_all(jobs)
|
|
19
|
+
BatchExecution.create_all_from_jobs(jobs) if Batch.migrated?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Also guards against the batches schema not being installed: without
|
|
24
|
+
# its migration, jobs don't even have a batch_id.
|
|
25
|
+
def batched?
|
|
26
|
+
Batch.migrated? && batch_id?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
def create_batch_execution
|
|
31
|
+
BatchExecution.create_all_from_jobs([ self ])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def update_batch_progress
|
|
35
|
+
return unless saved_change_to_finished_at? && finished_at.present?
|
|
36
|
+
|
|
37
|
+
batch_execution&.destroy!
|
|
38
|
+
rescue ActiveRecord::ActiveRecordError => e
|
|
39
|
+
SolidQueue.instrument(:batch_progress_error, batch_id: batch_id, job_id: id, error: e)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Destroy through Active Record instead of relying on the foreign
|
|
43
|
+
# key's cascade, so destroying the tracking row retries the batch
|
|
44
|
+
# completion check.
|
|
45
|
+
def destroy_batch_execution
|
|
46
|
+
batch_execution&.destroy!
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -18,6 +18,9 @@ module SolidQueue
|
|
|
18
18
|
|
|
19
19
|
class_methods do
|
|
20
20
|
def prepare_all_for_execution(jobs)
|
|
21
|
+
# Track before dispatch so conflict-discarded jobs count like single enqueues.
|
|
22
|
+
batch_all(jobs)
|
|
23
|
+
|
|
21
24
|
due, not_yet_due = jobs.partition(&:due?)
|
|
22
25
|
dispatch_all(due) + schedule_all(not_yet_due)
|
|
23
26
|
end
|
|
@@ -78,7 +81,8 @@ module SolidQueue
|
|
|
78
81
|
|
|
79
82
|
def finished!
|
|
80
83
|
if SolidQueue.preserve_finished_jobs?
|
|
81
|
-
touch
|
|
84
|
+
# update! rather than touch so the batch tracking callbacks run
|
|
85
|
+
update!(finished_at: Time.current)
|
|
82
86
|
else
|
|
83
87
|
destroy!
|
|
84
88
|
end
|
|
@@ -4,13 +4,19 @@ module SolidQueue
|
|
|
4
4
|
class Job < Record
|
|
5
5
|
class EnqueueError < StandardError; end
|
|
6
6
|
|
|
7
|
-
include Executable, Clearable, Recurrable
|
|
7
|
+
include Executable, Clearable, Recurrable, Batchable
|
|
8
8
|
|
|
9
9
|
serialize :arguments, coder: JSON
|
|
10
10
|
|
|
11
11
|
class << self
|
|
12
12
|
def enqueue_all(active_jobs)
|
|
13
|
-
|
|
13
|
+
# Bulk enqueues bypass ActiveJob#enqueue, so batch membership is captured here
|
|
14
|
+
current_batch_id = Batch.current_batch_id
|
|
15
|
+
|
|
16
|
+
active_jobs.each do |job|
|
|
17
|
+
job.scheduled_at ||= Time.current
|
|
18
|
+
job.batch_id = current_batch_id || job.batch_id
|
|
19
|
+
end
|
|
14
20
|
active_jobs_by_job_id = active_jobs.index_by(&:job_id)
|
|
15
21
|
|
|
16
22
|
transaction do
|
|
@@ -63,7 +69,9 @@ module SolidQueue
|
|
|
63
69
|
class_name: active_job.class.name,
|
|
64
70
|
arguments: active_job.serialize,
|
|
65
71
|
concurrency_key: active_job.concurrency_key
|
|
66
|
-
}
|
|
72
|
+
}.tap do |attributes|
|
|
73
|
+
attributes[:batch_id] = active_job.batch_id if Batch.migrated?
|
|
74
|
+
end
|
|
67
75
|
end
|
|
68
76
|
end
|
|
69
77
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Inspired by active_job/core.rb docs
|
|
4
|
+
# https://github.com/rails/rails/blob/1c2529b9a6ba5a1eff58be0d0373d7d9d401015b/activejob/lib/active_job/core.rb#L136
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module BatchId
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
attr_accessor :batch_id
|
|
11
|
+
attr_accessor :callback_batch_id
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Seed membership at construction for enqueue paths deferred until after the
|
|
15
|
+
# batch context ends, notably bulk enqueue. Enqueueing inside another batch
|
|
16
|
+
# rebinds membership; enqueueing without a batch preserves it.
|
|
17
|
+
# SolidQueue::Job.enqueue_all repeats this capture because bulk enqueue
|
|
18
|
+
# bypasses #enqueue.
|
|
19
|
+
def initialize(*arguments, **kwargs)
|
|
20
|
+
super
|
|
21
|
+
self.batch_id = SolidQueue::Batch.current_batch_id if solid_queue_job?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def enqueue(options = {})
|
|
25
|
+
self.batch_id = SolidQueue::Batch.current_batch_id || batch_id if solid_queue_job?
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def serialize
|
|
30
|
+
super.tap do |data|
|
|
31
|
+
data["batch_id"] = batch_id if batch_id
|
|
32
|
+
data["callback_batch_id"] = callback_batch_id if callback_batch_id
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def deserialize(job_data)
|
|
37
|
+
super
|
|
38
|
+
self.batch_id = job_data["batch_id"]
|
|
39
|
+
self.callback_batch_id = job_data["callback_batch_id"]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def batch
|
|
43
|
+
batch_id_to_load = callback_batch_id || batch_id
|
|
44
|
+
return if batch_id_to_load.nil?
|
|
45
|
+
return @batch if defined?(@batch) && @loaded_batch_id == batch_id_to_load
|
|
46
|
+
|
|
47
|
+
@loaded_batch_id = batch_id_to_load
|
|
48
|
+
@batch = SolidQueue::Batch.find_by(id: batch_id_to_load)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def solid_queue_job?
|
|
54
|
+
self.class.queue_adapter_name == "solid_queue"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -37,7 +37,9 @@ ActiveRecord::Schema[7.1].define(version: 1) do
|
|
|
37
37
|
t.string "concurrency_key"
|
|
38
38
|
t.datetime "created_at", null: false
|
|
39
39
|
t.datetime "updated_at", null: false
|
|
40
|
+
t.bigint "batch_id"
|
|
40
41
|
t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id"
|
|
42
|
+
t.index [ "batch_id" ], name: "index_solid_queue_jobs_on_batch_id"
|
|
41
43
|
t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name"
|
|
42
44
|
t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at"
|
|
43
45
|
t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering"
|
|
@@ -120,6 +122,35 @@ ActiveRecord::Schema[7.1].define(version: 1) do
|
|
|
120
122
|
t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true
|
|
121
123
|
end
|
|
122
124
|
|
|
125
|
+
create_table "solid_queue_batches", force: :cascade do |t|
|
|
126
|
+
t.string "active_job_batch_id"
|
|
127
|
+
t.string "description"
|
|
128
|
+
t.text "on_finish"
|
|
129
|
+
t.text "on_success"
|
|
130
|
+
t.text "on_failure"
|
|
131
|
+
t.text "metadata"
|
|
132
|
+
t.integer "total_jobs", default: 0, null: false
|
|
133
|
+
t.integer "completed_jobs", default: 0, null: false
|
|
134
|
+
t.integer "failed_jobs", default: 0, null: false
|
|
135
|
+
t.datetime "enqueued_at"
|
|
136
|
+
t.datetime "finished_at"
|
|
137
|
+
t.datetime "failed_at"
|
|
138
|
+
t.datetime "created_at", null: false
|
|
139
|
+
t.datetime "updated_at", null: false
|
|
140
|
+
t.index [ "active_job_batch_id" ], name: "index_solid_queue_batches_on_active_job_batch_id", unique: true
|
|
141
|
+
t.index [ "finished_at" ], name: "index_solid_queue_batches_on_finished_at"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
create_table "solid_queue_batch_executions", force: :cascade do |t|
|
|
145
|
+
t.bigint "job_id", null: false
|
|
146
|
+
t.bigint "batch_id", null: false
|
|
147
|
+
t.datetime "created_at", null: false
|
|
148
|
+
t.index [ "job_id" ], name: "index_solid_queue_batch_executions_on_job_id", unique: true
|
|
149
|
+
t.index [ "batch_id" ], name: "index_solid_queue_batch_executions_on_batch_id"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
add_foreign_key "solid_queue_batch_executions", "solid_queue_batches", column: "batch_id", on_delete: :cascade
|
|
153
|
+
add_foreign_key "solid_queue_batch_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
|
123
154
|
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
|
124
155
|
add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
|
125
156
|
add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class AddBatchesToSolidQueue < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
# Fresh installs create all of this with the base schema, so skip
|
|
4
|
+
# anything that already exists
|
|
5
|
+
add_column :solid_queue_jobs, :batch_id, :bigint, if_not_exists: true
|
|
6
|
+
add_index :solid_queue_jobs, :batch_id, if_not_exists: true
|
|
7
|
+
|
|
8
|
+
create_table :solid_queue_batches, if_not_exists: true do |t|
|
|
9
|
+
t.string :active_job_batch_id
|
|
10
|
+
t.string :description
|
|
11
|
+
t.text :on_finish
|
|
12
|
+
t.text :on_success
|
|
13
|
+
t.text :on_failure
|
|
14
|
+
t.text :metadata
|
|
15
|
+
t.integer :total_jobs, default: 0, null: false
|
|
16
|
+
t.integer :completed_jobs, default: 0, null: false
|
|
17
|
+
t.integer :failed_jobs, default: 0, null: false
|
|
18
|
+
t.datetime :enqueued_at
|
|
19
|
+
t.datetime :finished_at
|
|
20
|
+
t.datetime :failed_at
|
|
21
|
+
t.datetime :created_at, null: false
|
|
22
|
+
t.datetime :updated_at, null: false
|
|
23
|
+
|
|
24
|
+
t.index :active_job_batch_id, unique: true
|
|
25
|
+
t.index :finished_at
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
create_table :solid_queue_batch_executions, if_not_exists: true do |t|
|
|
29
|
+
t.bigint :job_id, null: false
|
|
30
|
+
t.bigint :batch_id, null: false
|
|
31
|
+
t.datetime :created_at, null: false
|
|
32
|
+
|
|
33
|
+
t.index :job_id, unique: true
|
|
34
|
+
t.index :batch_id
|
|
35
|
+
t.foreign_key :solid_queue_batches, column: :batch_id, on_delete: :cascade
|
|
36
|
+
t.foreign_key :solid_queue_jobs, column: :job_id, on_delete: :cascade
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,44 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SolidQueue
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
attr_reader :interval, :batch_size
|
|
8
|
-
|
|
4
|
+
# Kept for compatibility: concurrency maintenance runs on the shared
|
|
5
|
+
# Dispatcher::Maintenance timer, together with batch maintenance.
|
|
6
|
+
class Dispatcher::ConcurrencyMaintenance < Dispatcher::Maintenance
|
|
9
7
|
def initialize(interval, batch_size)
|
|
10
|
-
|
|
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
|
|
8
|
+
super(interval, batch_size, concurrency: true, batches: false)
|
|
29
9
|
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
10
|
end
|
|
44
11
|
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class Dispatcher::Maintenance
|
|
5
|
+
include AppExecutor
|
|
6
|
+
|
|
7
|
+
attr_reader :interval, :batch_size
|
|
8
|
+
|
|
9
|
+
def initialize(interval, batch_size, concurrency:, batches:)
|
|
10
|
+
@interval = interval
|
|
11
|
+
@batch_size = batch_size
|
|
12
|
+
@concurrency = concurrency
|
|
13
|
+
@batches = batches
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def concurrency?
|
|
17
|
+
@concurrency
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def batches?
|
|
21
|
+
@batches
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def metadata
|
|
25
|
+
{ concurrency_maintenance_interval: (interval if concurrency?), batch_maintenance: batches? }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def start
|
|
29
|
+
@maintenance_task = Concurrent::TimerTask.new(run_now: true, execution_interval: interval) do
|
|
30
|
+
if concurrency?
|
|
31
|
+
expire_semaphores
|
|
32
|
+
unblock_blocked_executions
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sweep_stalled_batches if batches?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
@maintenance_task.add_observer do |_, _, error|
|
|
39
|
+
handle_thread_error(error) if error
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@maintenance_task.execute
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stop
|
|
46
|
+
@maintenance_task&.shutdown
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
def expire_semaphores
|
|
51
|
+
wrap_in_app_executor do
|
|
52
|
+
Semaphore.expired.in_batches(of: batch_size, &:delete_all)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def unblock_blocked_executions
|
|
57
|
+
wrap_in_app_executor do
|
|
58
|
+
BlockedExecution.unblock(batch_size)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def sweep_stalled_batches
|
|
63
|
+
wrap_in_app_executor do
|
|
64
|
+
if Batch.migrated?
|
|
65
|
+
Batch.sweep_stalled(batch_size: batch_size)
|
|
66
|
+
else
|
|
67
|
+
warn_once_about_pending_batch_migrations
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def warn_once_about_pending_batch_migrations
|
|
73
|
+
unless @warned_about_pending_migrations
|
|
74
|
+
Batch.warn_about_pending_migrations
|
|
75
|
+
@warned_about_pending_migrations = true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -7,8 +7,8 @@ module SolidQueue
|
|
|
7
7
|
attr_reader :batch_size
|
|
8
8
|
|
|
9
9
|
after_boot :run_start_hooks
|
|
10
|
-
after_boot :
|
|
11
|
-
before_shutdown :
|
|
10
|
+
after_boot :start_maintenance
|
|
11
|
+
before_shutdown :stop_maintenance
|
|
12
12
|
before_shutdown :run_stop_hooks
|
|
13
13
|
after_shutdown :run_exit_hooks
|
|
14
14
|
|
|
@@ -17,17 +17,21 @@ module SolidQueue
|
|
|
17
17
|
|
|
18
18
|
@batch_size = options[:batch_size]
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Run both maintenance routines on one timer instead of another thread.
|
|
21
|
+
if options[:concurrency_maintenance] || options[:batch_maintenance]
|
|
22
|
+
@maintenance = Maintenance.new(options[:concurrency_maintenance_interval], options[:batch_size],
|
|
23
|
+
concurrency: options[:concurrency_maintenance], batches: options[:batch_maintenance])
|
|
24
|
+
end
|
|
21
25
|
|
|
22
26
|
super(**options)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def metadata
|
|
26
|
-
super.merge(batch_size: batch_size
|
|
30
|
+
super.merge(batch_size: batch_size).merge(maintenance&.metadata || {})
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
private
|
|
30
|
-
attr_reader :
|
|
34
|
+
attr_reader :maintenance
|
|
31
35
|
|
|
32
36
|
def poll
|
|
33
37
|
batch = dispatch_next_batch
|
|
@@ -41,12 +45,12 @@ module SolidQueue
|
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
|
|
44
|
-
def
|
|
45
|
-
|
|
48
|
+
def start_maintenance
|
|
49
|
+
maintenance&.start
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
def
|
|
49
|
-
|
|
52
|
+
def stop_maintenance
|
|
53
|
+
maintenance&.stop
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
def all_work_completed?
|
data/lib/solid_queue/engine.rb
CHANGED
|
@@ -41,6 +41,10 @@ module SolidQueue
|
|
|
41
41
|
initializer "solid_queue.active_job.extensions" do
|
|
42
42
|
ActiveSupport.on_load :active_job do
|
|
43
43
|
include ActiveJob::ConcurrencyControls
|
|
44
|
+
|
|
45
|
+
ActiveSupport.on_load :active_record do
|
|
46
|
+
ActiveJob::Base.include ActiveJob::BatchId
|
|
47
|
+
end
|
|
44
48
|
end
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -57,8 +57,7 @@ module SolidQueue
|
|
|
57
57
|
terminated_fork.mark_as_reaped
|
|
58
58
|
|
|
59
59
|
if !status.exited? || status.exitstatus.to_i > 0
|
|
60
|
-
|
|
61
|
-
release_claimed_jobs_by(terminated_fork, with_error: error)
|
|
60
|
+
attempt_to_release_claimed_jobs_by(terminated_fork, status)
|
|
62
61
|
end
|
|
63
62
|
end
|
|
64
63
|
|
|
@@ -73,14 +72,24 @@ module SolidQueue
|
|
|
73
72
|
if terminated_fork = process_instances.delete(pid)
|
|
74
73
|
terminated_fork.mark_as_reaped
|
|
75
74
|
payload[:fork] = terminated_fork
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
|
|
76
|
+
attempt_to_release_claimed_jobs_by(terminated_fork, status)
|
|
78
77
|
|
|
79
78
|
start_process(configured_processes.delete(pid))
|
|
80
79
|
end
|
|
81
80
|
end
|
|
82
81
|
end
|
|
83
82
|
|
|
83
|
+
# The database may be unreachable — likely the same reason the fork
|
|
84
|
+
# terminated. Neither starting a replacement nor shutting down can depend
|
|
85
|
+
# on it: the jobs claimed by the terminated fork will be failed when its
|
|
86
|
+
# stale registration is pruned once the database is back.
|
|
87
|
+
def attempt_to_release_claimed_jobs_by(terminated_fork, status)
|
|
88
|
+
release_claimed_jobs_by(terminated_fork, with_error: Processes::ProcessExitError.new(status))
|
|
89
|
+
rescue StandardError => error
|
|
90
|
+
handle_thread_error(error)
|
|
91
|
+
end
|
|
92
|
+
|
|
84
93
|
def all_processes_terminated?
|
|
85
94
|
process_instances.empty?
|
|
86
95
|
end
|
|
@@ -16,7 +16,10 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def fail_many_claimed(event)
|
|
19
|
-
|
|
19
|
+
attributes = event.payload.slice(:job_ids, :process_ids)
|
|
20
|
+
attributes[:error] = formatted_error(event.payload[:error]) if event.payload[:error]
|
|
21
|
+
|
|
22
|
+
warn formatted_event(event, action: "Fail claimed jobs", **attributes)
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def release_claimed(event)
|
|
@@ -39,6 +42,18 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
|
|
|
39
42
|
debug formatted_event(event, action: "Discard job", **event.payload.slice(:job_id, :status))
|
|
40
43
|
end
|
|
41
44
|
|
|
45
|
+
def finish_batch(event)
|
|
46
|
+
info formatted_event(event, action: "Finish batch", **event.payload.slice(:batch_id, :total_jobs, :completed_jobs, :failed_jobs))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def sweep_stalled_batches(event)
|
|
50
|
+
debug formatted_event(event, action: "Sweep stalled batches", **event.payload.slice(:stale_executions, :finished_batches, :started_batches))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def batch_progress_error(event)
|
|
54
|
+
error formatted_event(event, action: "Error updating batch progress", **event.payload.slice(:batch_id, :job_id), error: formatted_error(event.payload[:error]))
|
|
55
|
+
end
|
|
56
|
+
|
|
42
57
|
def release_many_blocked(event)
|
|
43
58
|
debug formatted_event(event, action: "Unblock jobs", **event.payload.slice(:limit, :size))
|
|
44
59
|
end
|
|
@@ -50,7 +50,7 @@ module SolidQueue::Processes
|
|
|
50
50
|
case
|
|
51
51
|
when running_as_fork?
|
|
52
52
|
@boot_guard = BootGuards::ForkGuard.new
|
|
53
|
-
|
|
53
|
+
create_fork(&block).tap { @boot_guard.start }
|
|
54
54
|
when running_async?
|
|
55
55
|
@boot_guard = BootGuards::NullGuard.new
|
|
56
56
|
@thread = create_thread(&block)
|
|
@@ -64,10 +64,7 @@ module SolidQueue::Processes
|
|
|
64
64
|
def boot
|
|
65
65
|
SolidQueue.instrument(:start_process, process: self) do
|
|
66
66
|
run_callbacks(:boot) do
|
|
67
|
-
if running_as_fork?
|
|
68
|
-
register_signal_handlers
|
|
69
|
-
set_procline
|
|
70
|
-
end
|
|
67
|
+
set_procline if running_as_fork?
|
|
71
68
|
end
|
|
72
69
|
end
|
|
73
70
|
|
|
@@ -39,9 +39,13 @@ module SolidQueue
|
|
|
39
39
|
run_start_hooks
|
|
40
40
|
|
|
41
41
|
start_processes
|
|
42
|
-
launch_maintenance_task
|
|
43
42
|
|
|
44
|
-
|
|
43
|
+
if stopped?
|
|
44
|
+
shutdown
|
|
45
|
+
else
|
|
46
|
+
launch_maintenance_task
|
|
47
|
+
supervise
|
|
48
|
+
end
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
def stop
|
|
@@ -65,27 +69,33 @@ module SolidQueue
|
|
|
65
69
|
end
|
|
66
70
|
|
|
67
71
|
def start_processes
|
|
68
|
-
configuration.configured_processes.each
|
|
72
|
+
configuration.configured_processes.each do |configured_process|
|
|
73
|
+
# Honour signals that arrive during boot or start hooks: a queued TERM
|
|
74
|
+
# stops us here, before starting children, instead of in #supervise,
|
|
75
|
+
# after all of them have been started
|
|
76
|
+
break if time_to_stop?
|
|
77
|
+
|
|
78
|
+
start_process(configured_process)
|
|
79
|
+
end
|
|
69
80
|
end
|
|
70
81
|
|
|
71
82
|
def supervise
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
set_procline
|
|
77
|
-
process_signal_queue
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
unless stopped?
|
|
81
|
-
check_and_replace_terminated_processes
|
|
82
|
-
interruptible_sleep(1.second)
|
|
83
|
-
end
|
|
83
|
+
until time_to_stop?
|
|
84
|
+
set_procline
|
|
85
|
+
check_and_replace_terminated_processes
|
|
86
|
+
interruptible_sleep(1.second)
|
|
84
87
|
end
|
|
85
88
|
ensure
|
|
86
89
|
shutdown
|
|
87
90
|
end
|
|
88
91
|
|
|
92
|
+
# Process any signals queued while we were busy and report whether
|
|
93
|
+
# we've been asked to stop
|
|
94
|
+
def time_to_stop?
|
|
95
|
+
process_signal_queue
|
|
96
|
+
stopped?
|
|
97
|
+
end
|
|
98
|
+
|
|
89
99
|
def start_process(configured_process)
|
|
90
100
|
process_instance = configured_process.instantiate.tap do |instance|
|
|
91
101
|
instance.supervised_by process
|
|
@@ -139,7 +149,10 @@ module SolidQueue
|
|
|
139
149
|
end
|
|
140
150
|
|
|
141
151
|
def set_procline
|
|
142
|
-
|
|
152
|
+
# Embedded supervisors don't own their process's title
|
|
153
|
+
if standalone?
|
|
154
|
+
procline "supervising #{configured_processes.keys.join(", ")}"
|
|
155
|
+
end
|
|
143
156
|
end
|
|
144
157
|
|
|
145
158
|
def sync_std_streams
|
data/lib/solid_queue/version.rb
CHANGED