solid_queue 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +230 -0
  4. data/Rakefile +8 -0
  5. data/app/models/solid_queue/blocked_execution.rb +68 -0
  6. data/app/models/solid_queue/claimed_execution.rb +73 -0
  7. data/app/models/solid_queue/execution/job_attributes.rb +24 -0
  8. data/app/models/solid_queue/execution.rb +15 -0
  9. data/app/models/solid_queue/failed_execution.rb +31 -0
  10. data/app/models/solid_queue/job/clearable.rb +19 -0
  11. data/app/models/solid_queue/job/concurrency_controls.rb +50 -0
  12. data/app/models/solid_queue/job/executable.rb +87 -0
  13. data/app/models/solid_queue/job.rb +38 -0
  14. data/app/models/solid_queue/pause.rb +6 -0
  15. data/app/models/solid_queue/process/prunable.rb +20 -0
  16. data/app/models/solid_queue/process.rb +28 -0
  17. data/app/models/solid_queue/queue.rb +52 -0
  18. data/app/models/solid_queue/queue_selector.rb +68 -0
  19. data/app/models/solid_queue/ready_execution.rb +41 -0
  20. data/app/models/solid_queue/record.rb +19 -0
  21. data/app/models/solid_queue/scheduled_execution.rb +65 -0
  22. data/app/models/solid_queue/semaphore.rb +65 -0
  23. data/config/routes.rb +2 -0
  24. data/db/migrate/20231211200639_create_solid_queue_tables.rb +100 -0
  25. data/lib/active_job/concurrency_controls.rb +53 -0
  26. data/lib/active_job/queue_adapters/solid_queue_adapter.rb +24 -0
  27. data/lib/generators/solid_queue/install/USAGE +9 -0
  28. data/lib/generators/solid_queue/install/install_generator.rb +19 -0
  29. data/lib/puma/plugin/solid_queue.rb +63 -0
  30. data/lib/solid_queue/app_executor.rb +21 -0
  31. data/lib/solid_queue/configuration.rb +102 -0
  32. data/lib/solid_queue/dispatcher.rb +73 -0
  33. data/lib/solid_queue/engine.rb +39 -0
  34. data/lib/solid_queue/pool.rb +58 -0
  35. data/lib/solid_queue/processes/base.rb +27 -0
  36. data/lib/solid_queue/processes/interruptible.rb +37 -0
  37. data/lib/solid_queue/processes/pidfile.rb +58 -0
  38. data/lib/solid_queue/processes/poller.rb +24 -0
  39. data/lib/solid_queue/processes/procline.rb +11 -0
  40. data/lib/solid_queue/processes/registrable.rb +69 -0
  41. data/lib/solid_queue/processes/runnable.rb +77 -0
  42. data/lib/solid_queue/processes/signals.rb +69 -0
  43. data/lib/solid_queue/processes/supervised.rb +38 -0
  44. data/lib/solid_queue/supervisor.rb +182 -0
  45. data/lib/solid_queue/tasks.rb +16 -0
  46. data/lib/solid_queue/version.rb +3 -0
  47. data/lib/solid_queue/worker.rb +54 -0
  48. data/lib/solid_queue.rb +52 -0
  49. 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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueue
4
+ class Pause < Record
5
+ end
6
+ 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,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -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,9 @@
1
+ Description:
2
+ Installs solid_queue as the Active Job's queue adapter
3
+
4
+ Example:
5
+ bin/rails generate solid_queue:install
6
+
7
+ This will create:
8
+ Installs solid_queue migrations
9
+ Replaces Active Job's adapter in envionment configuration
@@ -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