solid_queue 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +230 -0
- data/Rakefile +8 -0
- data/app/models/solid_queue/blocked_execution.rb +68 -0
- data/app/models/solid_queue/claimed_execution.rb +73 -0
- data/app/models/solid_queue/execution/job_attributes.rb +24 -0
- data/app/models/solid_queue/execution.rb +15 -0
- data/app/models/solid_queue/failed_execution.rb +31 -0
- data/app/models/solid_queue/job/clearable.rb +19 -0
- data/app/models/solid_queue/job/concurrency_controls.rb +50 -0
- data/app/models/solid_queue/job/executable.rb +87 -0
- data/app/models/solid_queue/job.rb +38 -0
- data/app/models/solid_queue/pause.rb +6 -0
- data/app/models/solid_queue/process/prunable.rb +20 -0
- data/app/models/solid_queue/process.rb +28 -0
- data/app/models/solid_queue/queue.rb +52 -0
- data/app/models/solid_queue/queue_selector.rb +68 -0
- data/app/models/solid_queue/ready_execution.rb +41 -0
- data/app/models/solid_queue/record.rb +19 -0
- data/app/models/solid_queue/scheduled_execution.rb +65 -0
- data/app/models/solid_queue/semaphore.rb +65 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20231211200639_create_solid_queue_tables.rb +100 -0
- data/lib/active_job/concurrency_controls.rb +53 -0
- data/lib/active_job/queue_adapters/solid_queue_adapter.rb +24 -0
- data/lib/generators/solid_queue/install/USAGE +9 -0
- data/lib/generators/solid_queue/install/install_generator.rb +19 -0
- data/lib/puma/plugin/solid_queue.rb +63 -0
- data/lib/solid_queue/app_executor.rb +21 -0
- data/lib/solid_queue/configuration.rb +102 -0
- data/lib/solid_queue/dispatcher.rb +73 -0
- data/lib/solid_queue/engine.rb +39 -0
- data/lib/solid_queue/pool.rb +58 -0
- data/lib/solid_queue/processes/base.rb +27 -0
- data/lib/solid_queue/processes/interruptible.rb +37 -0
- data/lib/solid_queue/processes/pidfile.rb +58 -0
- data/lib/solid_queue/processes/poller.rb +24 -0
- data/lib/solid_queue/processes/procline.rb +11 -0
- data/lib/solid_queue/processes/registrable.rb +69 -0
- data/lib/solid_queue/processes/runnable.rb +77 -0
- data/lib/solid_queue/processes/signals.rb +69 -0
- data/lib/solid_queue/processes/supervised.rb +38 -0
- data/lib/solid_queue/supervisor.rb +182 -0
- data/lib/solid_queue/tasks.rb +16 -0
- data/lib/solid_queue/version.rb +3 -0
- data/lib/solid_queue/worker.rb +54 -0
- data/lib/solid_queue.rb +52 -0
- metadata +134 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SolidQueue::Job < SolidQueue::Record
|
4
|
+
include Executable
|
5
|
+
|
6
|
+
if Gem::Version.new(Rails.version) >= Gem::Version.new("7.1")
|
7
|
+
serialize :arguments, coder: JSON
|
8
|
+
else
|
9
|
+
serialize :arguments, JSON
|
10
|
+
end
|
11
|
+
|
12
|
+
DEFAULT_PRIORITY = 0
|
13
|
+
DEFAULT_QUEUE_NAME = "default"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def enqueue_active_job(active_job, scheduled_at: Time.current)
|
17
|
+
enqueue \
|
18
|
+
queue_name: active_job.queue_name,
|
19
|
+
active_job_id: active_job.job_id,
|
20
|
+
priority: active_job.priority,
|
21
|
+
scheduled_at: scheduled_at,
|
22
|
+
class_name: active_job.class.name,
|
23
|
+
arguments: active_job.serialize,
|
24
|
+
concurrency_key: active_job.try(:concurrency_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def enqueue(**kwargs)
|
28
|
+
create!(**kwargs.compact.with_defaults(defaults)).tap do
|
29
|
+
SolidQueue.logger.debug "[SolidQueue] Enqueued job #{kwargs}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def defaults
|
35
|
+
{ queue_name: DEFAULT_QUEUE_NAME, priority: DEFAULT_PRIORITY }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue::Process::Prunable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
scope :prunable, -> { where("last_heartbeat_at <= ?", SolidQueue.process_alive_threshold.ago) }
|
8
|
+
end
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def prune
|
12
|
+
prunable.lock.find_in_batches(batch_size: 50) do |batch|
|
13
|
+
batch.each do |process|
|
14
|
+
SolidQueue.logger.info("[SolidQueue] Pruning dead process #{process.id} - #{process.metadata}")
|
15
|
+
process.deregister
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SolidQueue::Process < SolidQueue::Record
|
4
|
+
include Prunable
|
5
|
+
|
6
|
+
belongs_to :supervisor, class_name: "SolidQueue::Process", optional: true, inverse_of: :forks
|
7
|
+
has_many :forks, class_name: "SolidQueue::Process", inverse_of: :supervisor, foreign_key: :supervisor_id, dependent: :destroy
|
8
|
+
has_many :claimed_executions
|
9
|
+
|
10
|
+
store :metadata, coder: JSON
|
11
|
+
|
12
|
+
after_destroy -> { claimed_executions.release_all }
|
13
|
+
|
14
|
+
def self.register(**attributes)
|
15
|
+
create!(attributes.merge(last_heartbeat_at: Time.current))
|
16
|
+
end
|
17
|
+
|
18
|
+
def heartbeat
|
19
|
+
touch(:last_heartbeat_at)
|
20
|
+
end
|
21
|
+
|
22
|
+
def deregister
|
23
|
+
destroy!
|
24
|
+
rescue Exception
|
25
|
+
SolidQueue.logger.error("[SolidQueue] Error deregistering process #{id} - #{metadata}")
|
26
|
+
raise
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class Queue
|
5
|
+
attr_accessor :name
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def all
|
9
|
+
Job.select(:queue_name).distinct.collect do |job|
|
10
|
+
new(job.queue_name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def find_by_name(name)
|
15
|
+
new(name)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(name)
|
20
|
+
@name = name
|
21
|
+
end
|
22
|
+
|
23
|
+
def paused?
|
24
|
+
Pause.exists?(queue_name: name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def pause
|
28
|
+
Pause.create_or_find_by!(queue_name: name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def resume
|
32
|
+
Pause.where(queue_name: name).delete_all
|
33
|
+
end
|
34
|
+
|
35
|
+
def clear
|
36
|
+
Job.where(queue_name: name).each(&:discard)
|
37
|
+
end
|
38
|
+
|
39
|
+
def size
|
40
|
+
@size ||= ReadyExecution.queued_as(name).count
|
41
|
+
end
|
42
|
+
|
43
|
+
def ==(queue)
|
44
|
+
name == queue.name
|
45
|
+
end
|
46
|
+
alias_method :eql?, :==
|
47
|
+
|
48
|
+
def hash
|
49
|
+
name.hash
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class QueueSelector
|
5
|
+
attr_reader :raw_queues, :relation
|
6
|
+
|
7
|
+
def initialize(queue_list, relation)
|
8
|
+
@raw_queues = Array(queue_list).map { |queue| queue.to_s.strip }.presence || [ "*" ]
|
9
|
+
@relation = relation
|
10
|
+
end
|
11
|
+
|
12
|
+
def scoped_relations
|
13
|
+
case
|
14
|
+
when all? then [ relation.all ]
|
15
|
+
when none? then [ relation.none ]
|
16
|
+
else
|
17
|
+
queue_names.map { |queue_name| relation.queued_as(queue_name) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def all?
|
23
|
+
include_all_queues? && paused_queues.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def none?
|
27
|
+
queue_names.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def queue_names
|
31
|
+
@queue_names ||= eligible_queues - paused_queues
|
32
|
+
end
|
33
|
+
|
34
|
+
def eligible_queues
|
35
|
+
if include_all_queues? then all_queues
|
36
|
+
else
|
37
|
+
exact_names + prefixed_names
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def include_all_queues?
|
42
|
+
"*".in? raw_queues
|
43
|
+
end
|
44
|
+
|
45
|
+
def exact_names
|
46
|
+
raw_queues.select { |queue| !queue.include?("*") }
|
47
|
+
end
|
48
|
+
|
49
|
+
def prefixed_names
|
50
|
+
if prefixes.empty? then []
|
51
|
+
else
|
52
|
+
relation.where(([ "queue_name LIKE ?" ] * prefixes.count).join(" OR "), *prefixes).distinct(:queue_name).pluck(:queue_name)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def prefixes
|
57
|
+
@prefixes ||= raw_queues.select { |queue| queue.ends_with?("*") }.map { |queue| queue.tr("*", "%") }
|
58
|
+
end
|
59
|
+
|
60
|
+
def all_queues
|
61
|
+
relation.distinct(:queue_name).pluck(:queue_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def paused_queues
|
65
|
+
@paused_queues ||= Pause.all.pluck(:queue_name)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class ReadyExecution < Execution
|
5
|
+
scope :queued_as, ->(queue_name) { where(queue_name: queue_name) }
|
6
|
+
|
7
|
+
assume_attributes_from_job
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def claim(queue_list, limit, process_id)
|
11
|
+
QueueSelector.new(queue_list, self).scoped_relations.flat_map do |queue_relation|
|
12
|
+
select_and_lock(queue_relation, process_id, limit).tap do |locked|
|
13
|
+
limit -= locked.size
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def select_and_lock(queue_relation, process_id, limit)
|
20
|
+
return [] if limit <= 0
|
21
|
+
|
22
|
+
transaction do
|
23
|
+
job_ids = select_candidates(queue_relation, limit)
|
24
|
+
lock_candidates(job_ids, process_id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def select_candidates(queue_relation, limit)
|
29
|
+
queue_relation.ordered.limit(limit).lock.pluck(:job_id)
|
30
|
+
end
|
31
|
+
|
32
|
+
def lock_candidates(job_ids, process_id)
|
33
|
+
return [] if job_ids.none?
|
34
|
+
|
35
|
+
SolidQueue::ClaimedExecution.claiming(job_ids, process_id) do |claimed|
|
36
|
+
where(job_id: claimed.pluck(:job_id)).delete_all
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class Record < ActiveRecord::Base
|
5
|
+
self.abstract_class = true
|
6
|
+
|
7
|
+
connects_to **SolidQueue.connects_to if SolidQueue.connects_to
|
8
|
+
|
9
|
+
def self.lock(...)
|
10
|
+
if SolidQueue.use_skip_locked
|
11
|
+
super(Arel.sql("FOR UPDATE SKIP LOCKED"))
|
12
|
+
else
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveSupport.run_load_hooks :solid_queue_record, SolidQueue::Record
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SolidQueue
|
4
|
+
class ScheduledExecution < Execution
|
5
|
+
scope :due, -> { where(scheduled_at: ..Time.current) }
|
6
|
+
scope :ordered, -> { order(scheduled_at: :asc, priority: :asc) }
|
7
|
+
scope :next_batch, ->(batch_size) { due.ordered.limit(batch_size) }
|
8
|
+
|
9
|
+
assume_attributes_from_job :scheduled_at
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def dispatch_next_batch(batch_size)
|
13
|
+
transaction do
|
14
|
+
job_ids = next_batch(batch_size).lock.pluck(:job_id)
|
15
|
+
if job_ids.empty? then []
|
16
|
+
else
|
17
|
+
dispatch_batch(job_ids)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def dispatch_batch(job_ids)
|
24
|
+
jobs = Job.where(id: job_ids)
|
25
|
+
with_concurrency_limits, without_concurrency_limits = jobs.partition(&:concurrency_limited?)
|
26
|
+
|
27
|
+
dispatch_at_once(without_concurrency_limits)
|
28
|
+
dispatch_one_by_one(with_concurrency_limits)
|
29
|
+
|
30
|
+
successfully_dispatched(job_ids).tap do |dispatched_job_ids|
|
31
|
+
where(job_id: dispatched_job_ids).delete_all
|
32
|
+
SolidQueue.logger.info("[SolidQueue] Dispatched scheduled batch with #{dispatched_job_ids.size} jobs")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def dispatch_at_once(jobs)
|
37
|
+
ReadyExecution.insert_all ready_rows_from_batch(jobs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def dispatch_one_by_one(jobs)
|
41
|
+
jobs.each(&:dispatch)
|
42
|
+
end
|
43
|
+
|
44
|
+
def ready_rows_from_batch(jobs)
|
45
|
+
prepared_at = Time.current
|
46
|
+
|
47
|
+
jobs.map do |job|
|
48
|
+
{ job_id: job.id, queue_name: job.queue_name, priority: job.priority, created_at: prepared_at }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def successfully_dispatched(job_ids)
|
53
|
+
dispatched_and_ready(job_ids) + dispatched_and_blocked(job_ids)
|
54
|
+
end
|
55
|
+
|
56
|
+
def dispatched_and_ready(job_ids)
|
57
|
+
ReadyExecution.where(job_id: job_ids).pluck(:job_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def dispatched_and_blocked(job_ids)
|
61
|
+
BlockedExecution.where(job_id: job_ids).pluck(:job_id)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SolidQueue::Semaphore < SolidQueue::Record
|
4
|
+
scope :available, -> { where("value > 0") }
|
5
|
+
scope :expired, -> { where(expires_at: ...Time.current) }
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def wait(job)
|
9
|
+
Proxy.new(job, self).wait
|
10
|
+
end
|
11
|
+
|
12
|
+
def signal(job)
|
13
|
+
Proxy.new(job, self).signal
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Proxy
|
18
|
+
def initialize(job, proxied_class)
|
19
|
+
@job = job
|
20
|
+
@proxied_class = proxied_class
|
21
|
+
end
|
22
|
+
|
23
|
+
def wait
|
24
|
+
if semaphore = proxied_class.find_by(key: key)
|
25
|
+
semaphore.value > 0 && attempt_decrement
|
26
|
+
else
|
27
|
+
attempt_creation
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def signal
|
32
|
+
attempt_increment
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
attr_reader :job, :proxied_class
|
37
|
+
|
38
|
+
def attempt_creation
|
39
|
+
proxied_class.create!(key: key, value: limit - 1, expires_at: expires_at)
|
40
|
+
true
|
41
|
+
rescue ActiveRecord::RecordNotUnique
|
42
|
+
attempt_decrement
|
43
|
+
end
|
44
|
+
|
45
|
+
def attempt_decrement
|
46
|
+
proxied_class.available.where(key: key).update_all([ "value = value - 1, expires_at = ?", expires_at ]) > 0
|
47
|
+
end
|
48
|
+
|
49
|
+
def attempt_increment
|
50
|
+
proxied_class.where(key: key, value: ...limit).update_all([ "value = value + 1, expires_at = ?", expires_at ]) > 0
|
51
|
+
end
|
52
|
+
|
53
|
+
def key
|
54
|
+
job.concurrency_key
|
55
|
+
end
|
56
|
+
|
57
|
+
def expires_at
|
58
|
+
job.concurrency_duration.from_now
|
59
|
+
end
|
60
|
+
|
61
|
+
def limit
|
62
|
+
job.concurrency_limit
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
class CreateSolidQueueTables < ActiveRecord::Migration[7.1]
|
2
|
+
def change
|
3
|
+
create_table :solid_queue_jobs do |t|
|
4
|
+
t.string :queue_name, null: false
|
5
|
+
t.string :class_name, null: false, index: true
|
6
|
+
t.text :arguments
|
7
|
+
t.integer :priority, default: 0, null: false
|
8
|
+
t.string :active_job_id, index: true
|
9
|
+
t.datetime :scheduled_at
|
10
|
+
t.datetime :finished_at, index: true
|
11
|
+
t.string :concurrency_key
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
|
15
|
+
t.index [ :queue_name, :finished_at ], name: "index_solid_queue_jobs_for_filtering"
|
16
|
+
t.index [ :scheduled_at, :finished_at ], name: "index_solid_queue_jobs_for_alerting"
|
17
|
+
end
|
18
|
+
|
19
|
+
create_table :solid_queue_scheduled_executions do |t|
|
20
|
+
t.references :job, index: { unique: true }, null: false
|
21
|
+
t.string :queue_name, null: false
|
22
|
+
t.integer :priority, default: 0, null: false
|
23
|
+
t.datetime :scheduled_at, null: false
|
24
|
+
|
25
|
+
t.datetime :created_at, null: false
|
26
|
+
|
27
|
+
t.index [ :scheduled_at, :priority, :job_id ], name: "index_solid_queue_dispatch_all"
|
28
|
+
end
|
29
|
+
|
30
|
+
create_table :solid_queue_ready_executions do |t|
|
31
|
+
t.references :job, index: { unique: true }, null: false
|
32
|
+
t.string :queue_name, null: false
|
33
|
+
t.integer :priority, default: 0, null: false
|
34
|
+
|
35
|
+
t.datetime :created_at, null: false
|
36
|
+
|
37
|
+
t.index [ :priority, :job_id ], name: "index_solid_queue_poll_all"
|
38
|
+
t.index [ :queue_name, :priority, :job_id ], name: "index_solid_queue_poll_by_queue"
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table :solid_queue_claimed_executions do |t|
|
42
|
+
t.references :job, index: { unique: true }, null: false
|
43
|
+
t.bigint :process_id
|
44
|
+
t.datetime :created_at, null: false
|
45
|
+
|
46
|
+
t.index [ :process_id, :job_id ]
|
47
|
+
end
|
48
|
+
|
49
|
+
create_table :solid_queue_blocked_executions do |t|
|
50
|
+
t.references :job, index: { unique: true }, null: false
|
51
|
+
t.string :queue_name, null: false
|
52
|
+
t.integer :priority, default: 0, null: false
|
53
|
+
t.string :concurrency_key, null: false
|
54
|
+
t.datetime :expires_at, null: false
|
55
|
+
|
56
|
+
t.datetime :created_at, null: false
|
57
|
+
|
58
|
+
t.index [ :expires_at, :concurrency_key ], name: "index_solid_queue_blocked_executions_for_maintenance"
|
59
|
+
end
|
60
|
+
|
61
|
+
create_table :solid_queue_failed_executions do |t|
|
62
|
+
t.references :job, index: { unique: true }, null: false
|
63
|
+
t.text :error
|
64
|
+
t.datetime :created_at, null: false
|
65
|
+
end
|
66
|
+
|
67
|
+
create_table :solid_queue_pauses do |t|
|
68
|
+
t.string :queue_name, null: false, index: { unique: true }
|
69
|
+
t.datetime :created_at, null: false
|
70
|
+
end
|
71
|
+
|
72
|
+
create_table :solid_queue_processes do |t|
|
73
|
+
t.string :kind, null: false
|
74
|
+
t.datetime :last_heartbeat_at, null: false, index: true
|
75
|
+
t.bigint :supervisor_id, index: true
|
76
|
+
|
77
|
+
t.integer :pid, null: false
|
78
|
+
t.string :hostname
|
79
|
+
t.text :metadata
|
80
|
+
|
81
|
+
t.datetime :created_at, null: false
|
82
|
+
end
|
83
|
+
|
84
|
+
create_table :solid_queue_semaphores do |t|
|
85
|
+
t.string :key, null: false, index: { unique: true }
|
86
|
+
t.integer :value, default: 1, null: false
|
87
|
+
t.datetime :expires_at, null: false, index: true
|
88
|
+
|
89
|
+
t.timestamps
|
90
|
+
|
91
|
+
t.index [ :key, :value ], name: "index_solid_queue_semaphores_on_key_and_value"
|
92
|
+
end
|
93
|
+
|
94
|
+
add_foreign_key :solid_queue_blocked_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
|
95
|
+
add_foreign_key :solid_queue_claimed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
|
96
|
+
add_foreign_key :solid_queue_failed_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
|
97
|
+
add_foreign_key :solid_queue_ready_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
|
98
|
+
add_foreign_key :solid_queue_scheduled_executions, :solid_queue_jobs, column: :job_id, on_delete: :cascade
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module ConcurrencyControls
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
DEFAULT_CONCURRENCY_GROUP = ->(*) { self.class.name }
|
8
|
+
|
9
|
+
included do
|
10
|
+
class_attribute :concurrency_key, instance_accessor: false
|
11
|
+
class_attribute :concurrency_group, default: DEFAULT_CONCURRENCY_GROUP, instance_accessor: false
|
12
|
+
|
13
|
+
class_attribute :concurrency_limit
|
14
|
+
class_attribute :concurrency_duration, default: SolidQueue.default_concurrency_control_period
|
15
|
+
end
|
16
|
+
|
17
|
+
class_methods do
|
18
|
+
def limits_concurrency(key:, to: 1, group: DEFAULT_CONCURRENCY_GROUP, duration: SolidQueue.default_concurrency_control_period)
|
19
|
+
self.concurrency_key = key
|
20
|
+
self.concurrency_limit = to
|
21
|
+
self.concurrency_group = group
|
22
|
+
self.concurrency_duration = duration
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def concurrency_key
|
27
|
+
if self.class.concurrency_key
|
28
|
+
param = compute_concurrency_parameter(self.class.concurrency_key)
|
29
|
+
|
30
|
+
case param
|
31
|
+
when ActiveRecord::Base
|
32
|
+
[ concurrency_group, param.class.name, param.id ]
|
33
|
+
else
|
34
|
+
[ concurrency_group, param ]
|
35
|
+
end.compact.join("/")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def concurrency_group
|
41
|
+
compute_concurrency_parameter(self.class.concurrency_group)
|
42
|
+
end
|
43
|
+
|
44
|
+
def compute_concurrency_parameter(option)
|
45
|
+
case option
|
46
|
+
when String, Symbol
|
47
|
+
option.to_s
|
48
|
+
when Proc
|
49
|
+
instance_exec(*arguments, &option)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveJob
|
4
|
+
module QueueAdapters
|
5
|
+
# == Active Job SolidQueue adapter
|
6
|
+
#
|
7
|
+
# To use it set the queue_adapter config to +:solid_queue+.
|
8
|
+
#
|
9
|
+
# Rails.application.config.active_job.queue_adapter = :solid_queue
|
10
|
+
class SolidQueueAdapter
|
11
|
+
def enqueue(active_job) # :nodoc:
|
12
|
+
SolidQueue::Job.enqueue_active_job(active_job).tap do |job|
|
13
|
+
active_job.provider_job_id = job.id
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def enqueue_at(active_job, timestamp) # :nodoc:
|
18
|
+
SolidQueue::Job.enqueue_active_job(active_job, scheduled_at: Time.at(timestamp)).tap do |job|
|
19
|
+
active_job.provider_job_id = job.id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class SolidQueue::InstallGenerator < Rails::Generators::Base
|
4
|
+
class_option :skip_migrations, type: :boolean, default: nil, desc: "Skip migrations"
|
5
|
+
|
6
|
+
def add_solid_queue
|
7
|
+
%w[ development test production ].each do |env_name|
|
8
|
+
if (env_config = Pathname(destination_root).join("config/environments/#{env_name}.rb")).exist?
|
9
|
+
gsub_file env_config, /(# )?config\.active_job\.queue_adapter\s+=.*/, "config.active_job.queue_adapter = :solid_queue"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_migrations
|
15
|
+
unless options[:skip_migrations]
|
16
|
+
rails_command "railties:install:migrations FROM=solid_queue", inline: true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "puma/plugin"
|
2
|
+
|
3
|
+
Puma::Plugin.create do
|
4
|
+
attr_reader :puma_pid, :solid_queue_pid, :log_writer
|
5
|
+
|
6
|
+
def start(launcher)
|
7
|
+
@log_writer = launcher.log_writer
|
8
|
+
@puma_pid = $$
|
9
|
+
@solid_queue_pid = fork do
|
10
|
+
Thread.new { monitor_puma }
|
11
|
+
SolidQueue::Supervisor.start(mode: :all)
|
12
|
+
end
|
13
|
+
|
14
|
+
launcher.events.on_stopped { stop_solid_queue }
|
15
|
+
|
16
|
+
in_background do
|
17
|
+
monitor_solid_queue
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def stop_solid_queue
|
23
|
+
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
24
|
+
log "Stopping Solid Queue..."
|
25
|
+
Process.kill(:INT, solid_queue_pid) if solid_queue_pid
|
26
|
+
Process.wait(solid_queue_pid)
|
27
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
28
|
+
end
|
29
|
+
|
30
|
+
def monitor_puma
|
31
|
+
monitor(:puma_dead?, "Detected Puma has gone away, stopping Solid Queue...")
|
32
|
+
end
|
33
|
+
|
34
|
+
def monitor_solid_queue
|
35
|
+
monitor(:solid_queue_dead?, "Detected Solid Queue has gone away, stopping Puma...")
|
36
|
+
end
|
37
|
+
|
38
|
+
def monitor(process_dead, message)
|
39
|
+
loop do
|
40
|
+
if send(process_dead)
|
41
|
+
log message
|
42
|
+
Process.kill(:INT, $$)
|
43
|
+
break
|
44
|
+
end
|
45
|
+
sleep 2
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def solid_queue_dead?
|
50
|
+
Process.waitpid(solid_queue_pid, Process::WNOHANG)
|
51
|
+
false
|
52
|
+
rescue Errno::ECHILD, Errno::ESRCH
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def puma_dead?
|
57
|
+
Process.ppid != puma_pid
|
58
|
+
end
|
59
|
+
|
60
|
+
def log(...)
|
61
|
+
log_writer.log(...)
|
62
|
+
end
|
63
|
+
end
|