solid_queue 1.6.0 → 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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +133 -1
  3. data/UPGRADING.md +12 -0
  4. data/app/models/solid_queue/batch/callbacks.rb +50 -0
  5. data/app/models/solid_queue/batch/clearable.rb +23 -0
  6. data/app/models/solid_queue/batch/status.rb +64 -0
  7. data/app/models/solid_queue/batch/sweepable.rb +64 -0
  8. data/app/models/solid_queue/batch.rb +133 -0
  9. data/app/models/solid_queue/batch_execution.rb +52 -0
  10. data/app/models/solid_queue/claimed_execution.rb +1 -0
  11. data/app/models/solid_queue/failed_execution/batchable.rb +22 -0
  12. data/app/models/solid_queue/failed_execution.rb +1 -1
  13. data/app/models/solid_queue/job/batchable.rb +50 -0
  14. data/app/models/solid_queue/job/executable.rb +5 -1
  15. data/app/models/solid_queue/job.rb +11 -3
  16. data/lib/active_job/batch_id.rb +57 -0
  17. data/lib/generators/solid_queue/install/templates/db/queue_schema.rb +31 -0
  18. data/lib/generators/solid_queue/update/templates/db/add_batches_to_solid_queue.rb +39 -0
  19. data/lib/solid_queue/configuration.rb +2 -1
  20. data/lib/solid_queue/dispatcher/concurrency_maintenance.rb +4 -37
  21. data/lib/solid_queue/dispatcher/maintenance.rb +79 -0
  22. data/lib/solid_queue/dispatcher.rb +13 -9
  23. data/lib/solid_queue/engine.rb +4 -0
  24. data/lib/solid_queue/fork_supervisor.rb +13 -4
  25. data/lib/solid_queue/log_subscriber.rb +16 -1
  26. data/lib/solid_queue/processes/runnable.rb +2 -5
  27. data/lib/solid_queue/processes/supervised.rb +7 -0
  28. data/lib/solid_queue/supervisor/signals.rb +3 -0
  29. data/lib/solid_queue/supervisor.rb +29 -16
  30. data/lib/solid_queue/version.rb +1 -1
  31. data/lib/solid_queue.rb +1 -0
  32. metadata +13 -2
@@ -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(:finished_at)
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
- active_jobs.each { |job| job.scheduled_at ||= Time.current }
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
@@ -29,7 +29,8 @@ module SolidQueue
29
29
  batch_size: 500,
30
30
  polling_interval: 1,
31
31
  concurrency_maintenance: true,
32
- concurrency_maintenance_interval: 600
32
+ concurrency_maintenance_interval: 600,
33
+ batch_maintenance: true
33
34
  }
34
35
 
35
36
  SCHEDULER_DEFAULTS = {
@@ -1,44 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueue
4
- class Dispatcher::ConcurrencyMaintenance
5
- include AppExecutor
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
- @interval = interval
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 :start_concurrency_maintenance
11
- before_shutdown :stop_concurrency_maintenance
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
- @concurrency_maintenance = ConcurrencyMaintenance.new(options[:concurrency_maintenance_interval], options[:batch_size]) if options[:concurrency_maintenance]
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, concurrency_maintenance_interval: concurrency_maintenance&.interval)
30
+ super.merge(batch_size: batch_size).merge(maintenance&.metadata || {})
27
31
  end
28
32
 
29
33
  private
30
- attr_reader :concurrency_maintenance
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 start_concurrency_maintenance
45
- concurrency_maintenance&.start
48
+ def start_maintenance
49
+ maintenance&.start
46
50
  end
47
51
 
48
- def stop_concurrency_maintenance
49
- concurrency_maintenance&.stop
52
+ def stop_maintenance
53
+ maintenance&.stop
50
54
  end
51
55
 
52
56
  def all_work_completed?
@@ -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
 
@@ -57,8 +57,7 @@ module SolidQueue
57
57
  terminated_fork.mark_as_reaped
58
58
 
59
59
  if !status.exited? || status.exitstatus.to_i > 0
60
- error = Processes::ProcessExitError.new(status)
61
- release_claimed_jobs_by(terminated_fork, with_error: error)
60
+ attempt_to_release_claimed_jobs_by(terminated_fork, status)
62
61
  end
63
62
  end
64
63
 
@@ -73,14 +72,24 @@ module SolidQueue
73
72
  if terminated_fork = process_instances.delete(pid)
74
73
  terminated_fork.mark_as_reaped
75
74
  payload[:fork] = terminated_fork
76
- error = Processes::ProcessExitError.new(status)
77
- release_claimed_jobs_by(terminated_fork, with_error: error)
75
+
76
+ attempt_to_release_claimed_jobs_by(terminated_fork, status)
78
77
 
79
78
  start_process(configured_processes.delete(pid))
80
79
  end
81
80
  end
82
81
  end
83
82
 
83
+ # The database may be unreachable — likely the same reason the fork
84
+ # terminated. Neither starting a replacement nor shutting down can depend
85
+ # on it: the jobs claimed by the terminated fork will be failed when its
86
+ # stale registration is pruned once the database is back.
87
+ def attempt_to_release_claimed_jobs_by(terminated_fork, status)
88
+ release_claimed_jobs_by(terminated_fork, with_error: Processes::ProcessExitError.new(status))
89
+ rescue StandardError => error
90
+ handle_thread_error(error)
91
+ end
92
+
84
93
  def all_processes_terminated?
85
94
  process_instances.empty?
86
95
  end
@@ -16,7 +16,10 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
16
16
  end
17
17
 
18
18
  def fail_many_claimed(event)
19
- warn formatted_event(event, action: "Fail claimed jobs", **event.payload.slice(:job_ids, :process_ids))
19
+ attributes = event.payload.slice(:job_ids, :process_ids)
20
+ attributes[:error] = formatted_error(event.payload[:error]) if event.payload[:error]
21
+
22
+ warn formatted_event(event, action: "Fail claimed jobs", **attributes)
20
23
  end
21
24
 
22
25
  def release_claimed(event)
@@ -39,6 +42,18 @@ class SolidQueue::LogSubscriber < ActiveSupport::LogSubscriber
39
42
  debug formatted_event(event, action: "Discard job", **event.payload.slice(:job_id, :status))
40
43
  end
41
44
 
45
+ def finish_batch(event)
46
+ info formatted_event(event, action: "Finish batch", **event.payload.slice(:batch_id, :total_jobs, :completed_jobs, :failed_jobs))
47
+ end
48
+
49
+ def sweep_stalled_batches(event)
50
+ debug formatted_event(event, action: "Sweep stalled batches", **event.payload.slice(:stale_executions, :finished_batches, :started_batches))
51
+ end
52
+
53
+ def batch_progress_error(event)
54
+ error formatted_event(event, action: "Error updating batch progress", **event.payload.slice(:batch_id, :job_id), error: formatted_error(event.payload[:error]))
55
+ end
56
+
42
57
  def release_many_blocked(event)
43
58
  debug formatted_event(event, action: "Unblock jobs", **event.payload.slice(:limit, :size))
44
59
  end
@@ -50,7 +50,7 @@ module SolidQueue::Processes
50
50
  case
51
51
  when running_as_fork?
52
52
  @boot_guard = BootGuards::ForkGuard.new
53
- fork(&block).tap { @boot_guard.start }
53
+ create_fork(&block).tap { @boot_guard.start }
54
54
  when running_async?
55
55
  @boot_guard = BootGuards::NullGuard.new
56
56
  @thread = create_thread(&block)
@@ -64,10 +64,7 @@ module SolidQueue::Processes
64
64
  def boot
65
65
  SolidQueue.instrument(:start_process, process: self) do
66
66
  run_callbacks(:boot) do
67
- if running_as_fork?
68
- register_signal_handlers
69
- set_procline
70
- end
67
+ set_procline if running_as_fork?
71
68
  end
72
69
  end
73
70
 
@@ -25,6 +25,13 @@ module SolidQueue::Processes
25
25
  supervisor.present?
26
26
  end
27
27
 
28
+ def create_fork(&block)
29
+ fork do
30
+ register_signal_handlers
31
+ block.call
32
+ end
33
+ end
34
+
28
35
  def register_signal_handlers
29
36
  %w[ INT TERM ].each do |signal|
30
37
  trap(signal) do
@@ -29,6 +29,9 @@ module SolidQueue
29
29
  end
30
30
 
31
31
  def process_signal_queue
32
+ # Embedded supervisors don't own their process's signals
33
+ return unless standalone?
34
+
32
35
  while signal = signal_queue.shift
33
36
  handle_signal(signal)
34
37
  end
@@ -39,9 +39,13 @@ module SolidQueue
39
39
  run_start_hooks
40
40
 
41
41
  start_processes
42
- launch_maintenance_task
43
42
 
44
- supervise
43
+ if stopped?
44
+ shutdown
45
+ else
46
+ launch_maintenance_task
47
+ supervise
48
+ end
45
49
  end
46
50
 
47
51
  def stop
@@ -65,27 +69,33 @@ module SolidQueue
65
69
  end
66
70
 
67
71
  def start_processes
68
- configuration.configured_processes.each { |configured_process| start_process(configured_process) }
72
+ configuration.configured_processes.each do |configured_process|
73
+ # Honour signals that arrive during boot or start hooks: a queued TERM
74
+ # stops us here, before starting children, instead of in #supervise,
75
+ # after all of them have been started
76
+ break if time_to_stop?
77
+
78
+ start_process(configured_process)
79
+ end
69
80
  end
70
81
 
71
82
  def supervise
72
- loop do
73
- break if stopped?
74
-
75
- if standalone?
76
- set_procline
77
- process_signal_queue
78
- end
79
-
80
- unless stopped?
81
- check_and_replace_terminated_processes
82
- interruptible_sleep(1.second)
83
- end
83
+ until time_to_stop?
84
+ set_procline
85
+ check_and_replace_terminated_processes
86
+ interruptible_sleep(1.second)
84
87
  end
85
88
  ensure
86
89
  shutdown
87
90
  end
88
91
 
92
+ # Process any signals queued while we were busy and report whether
93
+ # we've been asked to stop
94
+ def time_to_stop?
95
+ process_signal_queue
96
+ stopped?
97
+ end
98
+
89
99
  def start_process(configured_process)
90
100
  process_instance = configured_process.instantiate.tap do |instance|
91
101
  instance.supervised_by process
@@ -139,7 +149,10 @@ module SolidQueue
139
149
  end
140
150
 
141
151
  def set_procline
142
- procline "supervising #{configured_processes.keys.join(", ")}"
152
+ # Embedded supervisors don't own their process's title
153
+ if standalone?
154
+ procline "supervising #{configured_processes.keys.join(", ")}"
155
+ end
143
156
  end
144
157
 
145
158
  def sync_std_streams
@@ -1,5 +1,5 @@
1
1
  module SolidQueue
2
- VERSION = "1.6.0"
2
+ VERSION = "1.7.0"
3
3
 
4
4
  def self.next_major_version
5
5
  Gem::Version.new(VERSION).segments.first + 1
data/lib/solid_queue.rb CHANGED
@@ -5,6 +5,7 @@ require "solid_queue/engine"
5
5
 
6
6
  require "active_job"
7
7
  require "active_job/queue_adapters"
8
+ require "active_job/batch_id"
8
9
 
9
10
  require "active_support"
10
11
  require "active_support/core_ext/numeric/time"