diff --git a/Gemfile.lock b/Gemfile.lock index 4e1c8c2b..72ec6ba5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - solid_queue (1.1.3) + solid_queue (1.1.4) activejob (>= 7.1) activerecord (>= 7.1) concurrent-ruby (>= 1.3.1) diff --git a/README.md b/README.md index c77ed953..cc610c53 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ and then remove the paused ones. Pausing in general should be something rare, us Do this: ```yml -queues: background, backend +queues: [ background, backend ] ``` instead of this: @@ -379,6 +379,8 @@ And into two different points in the worker's, dispatcher's and scheduler's life - `(worker|dispatcher|scheduler)_start`: after the worker/dispatcher/scheduler has finished booting and right before it starts the polling loop or loading the recurring schedule. - `(worker|dispatcher|scheduler)_stop`: after receiving a signal (`TERM`, `INT` or `QUIT`) and right before starting graceful or immediate shutdown (which is just `exit!`). +Each of these hooks has an instance of the supervisor/worker/dispatcher/scheduler yielded to the block so that you may read its configuration for logging or metrics reporting purposes. + You can use the following methods with a block to do this: ```ruby SolidQueue.on_start @@ -396,8 +398,20 @@ SolidQueue.on_scheduler_stop For example: ```ruby -SolidQueue.on_start { start_metrics_server } -SolidQueue.on_stop { stop_metrics_server } +SolidQueue.on_start do |supervisor| + MyMetricsReporter.process_name = supervisor.name + + start_metrics_server +end + +SolidQueue.on_stop do |_supervisor| + stop_metrics_server +end + +SolidQueue.on_worker_start do |worker| + MyMetricsReporter.process_name = worker.name + MyMetricsReporter.queues = worker.queues.join(',') +end ``` These can be called several times to add multiple hooks, but it needs to happen before Solid Queue is started. An initializer would be a good place to do this. diff --git a/app/models/solid_queue/claimed_execution.rb b/app/models/solid_queue/claimed_execution.rb index c2b13909..94ee3593 100644 --- a/app/models/solid_queue/claimed_execution.rb +++ b/app/models/solid_queue/claimed_execution.rb @@ -92,7 +92,7 @@ def failed_with(error) private def execute - ActiveJob::Base.execute(job.arguments) + ActiveJob::Base.execute(job.arguments.merge("provider_job_id" => job.id)) Result.new(true, nil) rescue Exception => e Result.new(false, e) diff --git a/lib/solid_queue.rb b/lib/solid_queue.rb index 1e1961e6..e0d51c8c 100644 --- a/lib/solid_queue.rb +++ b/lib/solid_queue.rb @@ -41,30 +41,20 @@ module SolidQueue mattr_accessor :clear_finished_jobs_after, default: 1.day mattr_accessor :default_concurrency_control_period, default: 3.minutes - delegate :on_start, :on_stop, to: Supervisor + delegate :on_start, :on_stop, :on_exit, to: Supervisor - def on_worker_start(...) - Worker.on_start(...) - end - - def on_worker_stop(...) - Worker.on_stop(...) - end - - def on_dispatcher_start(...) - Dispatcher.on_start(...) - end - - def on_dispatcher_stop(...) - Dispatcher.on_stop(...) - end + [ Dispatcher, Scheduler, Worker ].each do |process| + define_singleton_method(:"on_#{process.name.demodulize.downcase}_start") do |&block| + process.on_start(&block) + end - def on_scheduler_start(...) - Scheduler.on_start(...) - end + define_singleton_method(:"on_#{process.name.demodulize.downcase}_stop") do |&block| + process.on_stop(&block) + end - def on_scheduler_stop(...) - Scheduler.on_stop(...) + define_singleton_method(:"on_#{process.name.demodulize.downcase}_exit") do |&block| + process.on_exit(&block) + end end def supervisor? diff --git a/lib/solid_queue/dispatcher.rb b/lib/solid_queue/dispatcher.rb index a443df2e..1583e1dd 100644 --- a/lib/solid_queue/dispatcher.rb +++ b/lib/solid_queue/dispatcher.rb @@ -3,12 +3,13 @@ module SolidQueue class Dispatcher < Processes::Poller include LifecycleHooks - attr_accessor :batch_size, :concurrency_maintenance + attr_reader :batch_size after_boot :run_start_hooks after_boot :start_concurrency_maintenance before_shutdown :stop_concurrency_maintenance - after_shutdown :run_stop_hooks + before_shutdown :run_stop_hooks + after_shutdown :run_exit_hooks def initialize(**options) options = options.dup.with_defaults(SolidQueue::Configuration::DISPATCHER_DEFAULTS) @@ -25,6 +26,8 @@ def metadata end private + attr_reader :concurrency_maintenance + def poll batch = dispatch_next_batch diff --git a/lib/solid_queue/lifecycle_hooks.rb b/lib/solid_queue/lifecycle_hooks.rb index fabddac4..ec43b7a7 100644 --- a/lib/solid_queue/lifecycle_hooks.rb +++ b/lib/solid_queue/lifecycle_hooks.rb @@ -5,7 +5,7 @@ module LifecycleHooks extend ActiveSupport::Concern included do - mattr_reader :lifecycle_hooks, default: { start: [], stop: [] } + mattr_reader :lifecycle_hooks, default: { start: [], stop: [], exit: [] } end class_methods do @@ -17,7 +17,12 @@ def on_stop(&block) self.lifecycle_hooks[:stop] << block end + def on_exit(&block) + self.lifecycle_hooks[:exit] << block + end + def clear_hooks + self.lifecycle_hooks[:exit] = [] self.lifecycle_hooks[:start] = [] self.lifecycle_hooks[:stop] = [] end @@ -32,9 +37,13 @@ def run_stop_hooks run_hooks_for :stop end + def run_exit_hooks + run_hooks_for :exit + end + def run_hooks_for(event) self.class.lifecycle_hooks.fetch(event, []).each do |block| - block.call + block.call(self) rescue Exception => exception handle_thread_error(exception) end diff --git a/lib/solid_queue/processes/og_interruptible.rb b/lib/solid_queue/processes/og_interruptible.rb index d3b6e390..7eff46f1 100644 --- a/lib/solid_queue/processes/og_interruptible.rb +++ b/lib/solid_queue/processes/og_interruptible.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# frozen_string_literal: true - module SolidQueue::Processes # The original implementation of Interruptible that works # with Ruby 3.1 and earlier diff --git a/lib/solid_queue/processes/process_pruned_error.rb b/lib/solid_queue/processes/process_pruned_error.rb index ed699b68..3093e0d4 100644 --- a/lib/solid_queue/processes/process_pruned_error.rb +++ b/lib/solid_queue/processes/process_pruned_error.rb @@ -4,7 +4,7 @@ module SolidQueue module Processes class ProcessPrunedError < RuntimeError def initialize(last_heartbeat_at) - super("Process was found dead and pruned (last heartbeat at: #{last_heartbeat_at}") + super("Process was found dead and pruned (last heartbeat at: #{last_heartbeat_at})") end end end diff --git a/lib/solid_queue/scheduler.rb b/lib/solid_queue/scheduler.rb index b68075dc..3cec90fa 100644 --- a/lib/solid_queue/scheduler.rb +++ b/lib/solid_queue/scheduler.rb @@ -5,12 +5,13 @@ class Scheduler < Processes::Base include Processes::Runnable include LifecycleHooks - attr_accessor :recurring_schedule + attr_reader :recurring_schedule after_boot :run_start_hooks after_boot :schedule_recurring_tasks before_shutdown :unschedule_recurring_tasks before_shutdown :run_stop_hooks + after_shutdown :run_exit_hooks def initialize(recurring_tasks:, **options) @recurring_schedule = RecurringSchedule.new(recurring_tasks) diff --git a/lib/solid_queue/supervisor.rb b/lib/solid_queue/supervisor.rb index e8f075eb..f2207691 100644 --- a/lib/solid_queue/supervisor.rb +++ b/lib/solid_queue/supervisor.rb @@ -5,6 +5,8 @@ class Supervisor < Processes::Base include LifecycleHooks include Maintenance, Signals, Pidfiled + after_shutdown :run_exit_hooks + class << self def start(**options) SolidQueue.supervisor = true diff --git a/lib/solid_queue/version.rb b/lib/solid_queue/version.rb index 01f8a592..86d3c2e6 100644 --- a/lib/solid_queue/version.rb +++ b/lib/solid_queue/version.rb @@ -1,3 +1,3 @@ module SolidQueue - VERSION = "1.1.3" + VERSION = "1.1.4" end diff --git a/lib/solid_queue/worker.rb b/lib/solid_queue/worker.rb index f34a14f0..e036a5fd 100644 --- a/lib/solid_queue/worker.rb +++ b/lib/solid_queue/worker.rb @@ -6,14 +6,16 @@ class Worker < Processes::Poller after_boot :run_start_hooks before_shutdown :run_stop_hooks + after_shutdown :run_exit_hooks - - attr_accessor :queues, :pool + attr_reader :queues, :pool def initialize(**options) options = options.dup.with_defaults(SolidQueue::Configuration::WORKER_DEFAULTS) - @queues = Array(options[:queues]) + # Ensure that the queues array is deep frozen to prevent accidental modification + @queues = Array(options[:queues]).map(&:freeze).freeze + @pool = Pool.new(options[:threads], on_idle: -> { wake_up }) super(**options) diff --git a/test/dummy/app/jobs/provider_job_id_job.rb b/test/dummy/app/jobs/provider_job_id_job.rb new file mode 100644 index 00000000..6992fcab --- /dev/null +++ b/test/dummy/app/jobs/provider_job_id_job.rb @@ -0,0 +1,5 @@ +class ProviderJobIdJob < ApplicationJob + def perform + JobBuffer.add "provider_job_id: #{provider_job_id}" + end +end diff --git a/test/integration/lifecycle_hooks_test.rb b/test/integration/lifecycle_hooks_test.rb index 8bc4dc94..b2fd50da 100644 --- a/test/integration/lifecycle_hooks_test.rb +++ b/test/integration/lifecycle_hooks_test.rb @@ -6,31 +6,95 @@ class LifecycleHooksTest < ActiveSupport::TestCase self.use_transactional_tests = false test "run lifecycle hooks" do - SolidQueue.on_start { JobResult.create!(status: :hook_called, value: :start) } - SolidQueue.on_stop { JobResult.create!(status: :hook_called, value: :stop) } + SolidQueue.on_start do |s| + name = s.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_start") + end - SolidQueue.on_worker_start { JobResult.create!(status: :hook_called, value: :worker_start) } - SolidQueue.on_worker_stop { JobResult.create!(status: :hook_called, value: :worker_stop) } + SolidQueue.on_stop do |s| + name = s.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_stop") + end - SolidQueue.on_dispatcher_start { JobResult.create!(status: :hook_called, value: :dispatcher_start) } - SolidQueue.on_dispatcher_stop { JobResult.create!(status: :hook_called, value: :dispatcher_stop) } + SolidQueue.on_exit do |s| + name = s.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_exit") + end - SolidQueue.on_scheduler_start { JobResult.create!(status: :hook_called, value: :scheduler_start) } - SolidQueue.on_scheduler_stop { JobResult.create!(status: :hook_called, value: :scheduler_stop) } + SolidQueue.on_worker_start do |w| + name = w.class.name.demodulize.downcase + queues = w.queues.join("_") + JobResult.create!(status: :hook_called, value: "#{name}_#{queues}_start") + end - pid = run_supervisor_as_fork(workers: [ { queues: "*" } ], dispatchers: [ { batch_size: 100 } ], skip_recurring: false) - wait_for_registered_processes(4) + SolidQueue.on_worker_stop do |w| + name = w.class.name.demodulize.downcase + queues = w.queues.join("_") + JobResult.create!(status: :hook_called, value: "#{name}_#{queues}_stop") + end + + SolidQueue.on_worker_exit do |w| + name = w.class.name.demodulize.downcase + queues = w.queues.join("_") + JobResult.create!(status: :hook_called, value: "#{name}_#{queues}_exit") + end + + SolidQueue.on_dispatcher_start do |d| + name = d.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_#{d.batch_size}_start") + end + + SolidQueue.on_dispatcher_stop do |d| + name = d.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_#{d.batch_size}_stop") + end + + SolidQueue.on_dispatcher_exit do |d| + name = d.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_#{d.batch_size}_exit") + end + + SolidQueue.on_scheduler_start do |s| + name = s.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_start") + end + + SolidQueue.on_scheduler_stop do |s| + name = s.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_stop") + end + + SolidQueue.on_scheduler_exit do |s| + name = s.class.name.demodulize.downcase + JobResult.create!(status: :hook_called, value: "#{name}_exit") + end + + pid = run_supervisor_as_fork( + workers: [ { queues: "first_queue" }, { queues: "second_queue", processes: 1 } ], + dispatchers: [ { batch_size: 100 } ], + skip_recurring: false + ) + + wait_for_registered_processes(5) terminate_process(pid) wait_for_registered_processes(0) + results = skip_active_record_query_cache do - assert_equal 8, JobResult.count - JobResult.last(8) + job_results = JobResult.where(status: :hook_called) + assert_equal 15, job_results.count + job_results end - assert_equal({ "hook_called" => 8 }, results.map(&:status).tally) - assert_equal %w[start stop worker_start worker_stop dispatcher_start dispatcher_stop scheduler_start scheduler_stop].sort, results.map(&:value).sort + assert_equal({ "hook_called" => 15 }, results.map(&:status).tally) + assert_equal %w[ + supervisor_start supervisor_stop supervisor_exit + worker_first_queue_start worker_first_queue_stop worker_first_queue_exit + worker_second_queue_start worker_second_queue_stop worker_second_queue_exit + dispatcher_100_start dispatcher_100_stop dispatcher_100_exit + scheduler_start scheduler_stop scheduler_exit + ].sort, results.map(&:value).sort ensure SolidQueue::Supervisor.clear_hooks SolidQueue::Worker.clear_hooks diff --git a/test/models/solid_queue/claimed_execution_test.rb b/test/models/solid_queue/claimed_execution_test.rb index b7892b21..98513c94 100644 --- a/test/models/solid_queue/claimed_execution_test.rb +++ b/test/models/solid_queue/claimed_execution_test.rb @@ -73,6 +73,14 @@ class SolidQueue::ClaimedExecutionTest < ActiveSupport::TestCase assert job.reload.failed? end + test "provider_job_id is available within job execution" do + job = ProviderJobIdJob.perform_later + claimed_execution = prepare_and_claim_job job + claimed_execution.perform + + assert_equal "provider_job_id: #{job.provider_job_id}", JobBuffer.last_value + end + private def prepare_and_claim_job(active_job, process: @process) job = SolidQueue::Job.find_by(active_job_id: active_job.job_id)