solid_queue 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|