solid_queue 1.5.1 → 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 +163 -6
- 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 +76 -11
- 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/fiber_pool.rb +130 -0
- data/lib/solid_queue/fork_supervisor.rb +13 -4
- data/lib/solid_queue/log_subscriber.rb +16 -1
- data/lib/solid_queue/pool.rb +46 -25
- 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/thread_pool.rb +28 -0
- data/lib/solid_queue/version.rb +1 -1
- data/lib/solid_queue/worker.rb +9 -3
- data/lib/solid_queue.rb +1 -0
- metadata +29 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class BatchExecution < Execution
|
|
5
|
+
self.assumable_attributes_from_job = [ :batch_id ]
|
|
6
|
+
|
|
7
|
+
belongs_to :batch
|
|
8
|
+
|
|
9
|
+
scope :with_finished_jobs, -> { joins(:job).merge(SolidQueue::Job.finished) }
|
|
10
|
+
scope :with_failed_jobs, -> { joins(job: :failed_execution) }
|
|
11
|
+
|
|
12
|
+
after_commit :finish_batch, on: :destroy
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def create_all_from_jobs(jobs)
|
|
16
|
+
jobs.select(&:batched?).group_by(&:batch_id).each do |batch_id, jobs_in_batch|
|
|
17
|
+
# Update the counter first: inserting tracking rows takes a shared FK lock on
|
|
18
|
+
# the batch row, then incrementing can deadlock concurrent MySQL adders.
|
|
19
|
+
if attempt_to_update_total_jobs(batch_id, jobs_in_batch)
|
|
20
|
+
super jobs_in_batch
|
|
21
|
+
else
|
|
22
|
+
raise Batch::AlreadyFinished, "Can't add jobs into an already finished batch"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
def attempt_to_update_total_jobs(batch_id, jobs)
|
|
29
|
+
new_jobs_count = count_new_jobs_among(jobs)
|
|
30
|
+
updated = SolidQueue::Batch.where(id: batch_id).unfinished.update_all([ "total_jobs = total_jobs + ?", new_jobs_count ])
|
|
31
|
+
updated > 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A job that has executed before was already counted when it first joined
|
|
35
|
+
# the batch: retries keep their active_job_id and batch across re-enqueues.
|
|
36
|
+
# This might undercount jobs whose retries switch to another batch, but that
|
|
37
|
+
# should be a rare enough case. The counter is used only for report/info, so
|
|
38
|
+
# we favour simplicity here
|
|
39
|
+
def count_new_jobs_among(jobs)
|
|
40
|
+
jobs.reject { |job| job.arguments["executions"].to_i > 0 }.map(&:active_job_id).uniq.size
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
def finish_batch
|
|
46
|
+
# Skip the serialized callback and metadata columns on this hot path
|
|
47
|
+
if batch = Batch.select(:id, :finished_at, :enqueued_at).find_by(id: batch_id)
|
|
48
|
+
batch.finish
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueue
|
|
4
|
+
class FailedExecution
|
|
5
|
+
# A FailedExecution is created only after retries are exhausted, when the
|
|
6
|
+
# job stops counting as pending in its batch.
|
|
7
|
+
module Batchable
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
after_create :destroy_job_batch_execution, if: -> { Batch.migrated? && job.batch_id? }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
def destroy_job_batch_execution
|
|
16
|
+
job.batch_execution&.destroy!
|
|
17
|
+
rescue ActiveRecord::ActiveRecordError => e
|
|
18
|
+
SolidQueue.instrument(:batch_progress_error, batch_id: job.batch_id, job_id: job.id, error: e)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -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
|
|
@@ -6,7 +6,9 @@ module SolidQueue
|
|
|
6
6
|
include ActiveModel::Validations::Callbacks
|
|
7
7
|
|
|
8
8
|
validate :ensure_configured_processes, :ensure_valid_recurring_tasks
|
|
9
|
-
validate :
|
|
9
|
+
validate :ensure_valid_worker_execution_options
|
|
10
|
+
validate :ensure_fiber_workers_have_required_dependency, :ensure_fiber_workers_use_supported_isolation_level
|
|
11
|
+
validate :warn_about_incorrectly_sized_database_pool, :warn_about_missing_config_files
|
|
10
12
|
|
|
11
13
|
before_validation { warnings.clear }
|
|
12
14
|
|
|
@@ -27,7 +29,8 @@ module SolidQueue
|
|
|
27
29
|
batch_size: 500,
|
|
28
30
|
polling_interval: 1,
|
|
29
31
|
concurrency_maintenance: true,
|
|
30
|
-
concurrency_maintenance_interval: 600
|
|
32
|
+
concurrency_maintenance_interval: 600,
|
|
33
|
+
batch_maintenance: true
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
SCHEDULER_DEFAULTS = {
|
|
@@ -37,6 +40,7 @@ module SolidQueue
|
|
|
37
40
|
|
|
38
41
|
DEFAULT_CONFIG_FILE_PATH = "config/queue.yml"
|
|
39
42
|
DEFAULT_RECURRING_SCHEDULE_FILE_PATH = "config/recurring.yml"
|
|
43
|
+
FIBER_QUERY_SCOPED_CONNECTIONS_VERSION = Gem::Version.new("7.2.0")
|
|
40
44
|
|
|
41
45
|
def initialize(**options)
|
|
42
46
|
@options = options.with_defaults(default_options)
|
|
@@ -99,12 +103,12 @@ module SolidQueue
|
|
|
99
103
|
end
|
|
100
104
|
end
|
|
101
105
|
|
|
102
|
-
def
|
|
106
|
+
def warn_about_incorrectly_sized_database_pool
|
|
103
107
|
db_pool_size = SolidQueue::Record.connection_pool&.size
|
|
104
108
|
|
|
105
|
-
if db_pool_size && db_pool_size <
|
|
106
|
-
warnings.add(:base, "Warning: Solid Queue
|
|
107
|
-
"database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
|
|
109
|
+
if db_pool_size && db_pool_size < estimated_database_pool_size
|
|
110
|
+
warnings.add(:base, "Warning: Solid Queue needs at least #{estimated_database_pool_size} database connections " \
|
|
111
|
+
"for the configured workers but the database connection pool is #{db_pool_size}. Increase it in `config/database.yml`")
|
|
108
112
|
end
|
|
109
113
|
rescue ActiveRecord::ActiveRecordError
|
|
110
114
|
# No usable database connection. Skip the pool-size warning in that case.
|
|
@@ -121,6 +125,34 @@ module SolidQueue
|
|
|
121
125
|
end
|
|
122
126
|
end
|
|
123
127
|
|
|
128
|
+
def ensure_valid_worker_execution_options
|
|
129
|
+
workers_options.each do |options|
|
|
130
|
+
if options.key?(:threads) && options.key?(:fibers)
|
|
131
|
+
errors.add(:base, "Workers can specify either `threads` or `fibers`, but not both.")
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def ensure_fiber_workers_have_required_dependency
|
|
137
|
+
return unless workers_options.any? { |options| fiber_worker?(options) }
|
|
138
|
+
|
|
139
|
+
require "async"
|
|
140
|
+
require "async/semaphore"
|
|
141
|
+
rescue LoadError
|
|
142
|
+
errors.add(:base, "Fiber workers require the `async` gem. " \
|
|
143
|
+
"Add `gem \"async\"` to your Gemfile to configure workers with `fibers`.")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def ensure_fiber_workers_use_supported_isolation_level
|
|
147
|
+
return unless workers_options.any? { |options| fiber_worker?(options) }
|
|
148
|
+
|
|
149
|
+
unless ActiveSupport::IsolatedExecutionState.isolation_level == :fiber
|
|
150
|
+
errors.add(:base, "Fiber workers require fiber-scoped isolated execution state. " \
|
|
151
|
+
"Set `config.active_support.isolation_level = :fiber` in your Rails configuration " \
|
|
152
|
+
"(or `ActiveSupport::IsolatedExecutionState.isolation_level = :fiber` outside Rails).")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
124
156
|
def default_options
|
|
125
157
|
{
|
|
126
158
|
mode: ENV["SOLID_QUEUE_SUPERVISOR_MODE"] || :fork,
|
|
@@ -162,7 +194,8 @@ module SolidQueue
|
|
|
162
194
|
1
|
|
163
195
|
end
|
|
164
196
|
|
|
165
|
-
|
|
197
|
+
defaults = worker_defaults_for(worker_options)
|
|
198
|
+
processes.times.map { Process.new(:worker, worker_options.with_defaults(defaults)) }
|
|
166
199
|
end
|
|
167
200
|
end
|
|
168
201
|
|
|
@@ -256,10 +289,42 @@ module SolidQueue
|
|
|
256
289
|
end
|
|
257
290
|
end
|
|
258
291
|
|
|
259
|
-
def
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
292
|
+
def estimated_database_pool_size
|
|
293
|
+
worker_pool_size = workers_options.map { |options| estimated_database_pool_size_for_worker(options) }.max
|
|
294
|
+
worker_pool_size || 1
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def estimated_database_pool_size_for_worker(options)
|
|
298
|
+
# Connections used to execute jobs + 1 for the worker's polling thread + 1 for the heartbeat task
|
|
299
|
+
estimated_execution_connections_for_worker(options) + 2
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def worker_capacity(options)
|
|
303
|
+
options[:fibers] || options[:threads] || WORKER_DEFAULTS[:threads]
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def estimated_execution_connections_for_worker(options)
|
|
307
|
+
fiber_worker?(options) ? fiber_execution_connections_for_worker(options) : worker_capacity(options)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def fiber_execution_connections_for_worker(options)
|
|
311
|
+
fiber_jobs_release_connections_between_queries? ? 1 : worker_capacity(options)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def fiber_jobs_release_connections_between_queries?
|
|
315
|
+
ActiveRecord.gem_version >= FIBER_QUERY_SCOPED_CONNECTIONS_VERSION
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def fiber_worker?(options)
|
|
319
|
+
options.key?(:fibers)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def worker_defaults_for(options)
|
|
323
|
+
if fiber_worker?(options)
|
|
324
|
+
WORKER_DEFAULTS.except(:threads)
|
|
325
|
+
else
|
|
326
|
+
WORKER_DEFAULTS
|
|
327
|
+
end
|
|
263
328
|
end
|
|
264
329
|
end
|
|
265
330
|
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
|
|