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.
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