diff --git a/README.md b/README.md index 4367c6b0..a8f5613a 100644 --- a/README.md +++ b/README.md @@ -453,6 +453,7 @@ There are several settings that control how Solid Queue works that you can set a - `preserve_finished_jobs`: whether to keep finished jobs in the `solid_queue_jobs` table—defaults to `true`. - `clear_finished_jobs_after`: period to keep finished jobs around, in case `preserve_finished_jobs` is true — defaults to 1 day. When installing Solid Queue, [a recurring job](#recurring-tasks) is automatically configured to clear finished jobs every hour on the 12th minute in batches. You can edit the `recurring.yml` configuration to change this as you see fit. - `default_concurrency_control_period`: the value to be used as the default for the `duration` parameter in [concurrency controls](#concurrency-controls). It defaults to 3 minutes. +- `recurring_task_job_class`: the name of the job class that runs [recurring tasks](#recurring-tasks) defined as a `command`—defaults to `"SolidQueue::RecurringJob"`. ### Validating the configuration @@ -916,13 +917,13 @@ MyJob.perform_later(42, status: "custom_status") Tasks are enqueued at their corresponding times by the scheduler, and each task schedules the next one. This is pretty much [inspired by what GoodJob does](https://github.com/bensheldon/good_job/blob/994ecff5323bf0337e10464841128fda100750e6/lib/good_job/cron_manager.rb). -For recurring tasks defined as a `command`, you can also change the job class that runs them as follows: +For recurring tasks defined as a `command`, you can also change the job class that runs them by setting its name in `config/application.rb` or your environment config: ```ruby -Rails.application.config.after_initialize do # or to_prepare - SolidQueue::RecurringTask.default_job_class = MyRecurringCommandJob -end +config.solid_queue.recurring_task_job_class = "MyRecurringCommandJob" ``` +The class is resolved when a task is first enqueued, so your app's job class isn't loaded at boot. + It's possible to run multiple schedulers with the same `recurring_tasks` configuration, for example, if you have multiple servers for redundancy, and you run the `scheduler` in more than one of them. To avoid enqueuing duplicate tasks at the same time, an entry in a new `solid_queue_recurring_executions` table is created in the same transaction as the job is enqueued. This table has a unique index on `task_key` and `run_at`, ensuring only one entry per task per time will be created. This only works if you have `preserve_finished_jobs` set to `true` (the default), and the guarantee applies as long as you keep the jobs around. **Note**: a single recurring schedule is supported, so you can have multiple schedulers using the same schedule, but not multiple schedulers using different configurations. diff --git a/app/models/solid_queue/recurring_task.rb b/app/models/solid_queue/recurring_task.rb index 40a5531e..4f148cb3 100644 --- a/app/models/solid_queue/recurring_task.rb +++ b/app/models/solid_queue/recurring_task.rb @@ -15,10 +15,17 @@ class RecurringTask < Record has_many :recurring_executions, foreign_key: :task_key, primary_key: :key - mattr_accessor :default_job_class - self.default_job_class = RecurringJob - class << self + def default_job_class + warn_about_default_job_class_deprecation + SolidQueue.recurring_task_job_class + end + + def default_job_class=(job_class) + warn_about_default_job_class_deprecation + SolidQueue.recurring_task_job_class = job_class + end + def wrap(args) args.is_a?(self) ? args : from_configuration(args.first, **args.second) end @@ -54,6 +61,14 @@ def create_or_update_all(tasks) upsert_all tasks.map(&:attributes_for_upsert) end end + + private + def warn_about_default_job_class_deprecation + SolidQueue.deprecator.warn(<<~DEPRECATION) + `SolidQueue::RecurringTask.default_job_class` is deprecated and will be removed in Solid Queue #{SolidQueue.next_major_version}.0. + Use `config.solid_queue.recurring_task_job_class` or `SolidQueue.recurring_task_job_class` instead. + DEPRECATION + end end @@ -190,7 +205,12 @@ def apply_default_time_zone_to(schedule) end def job_class - @job_class ||= class_name.present? ? class_name.safe_constantize : self.class.default_job_class + @job_class ||= class_name.present? ? class_name.safe_constantize : resolve_recurring_task_job_class + end + + def resolve_recurring_task_job_class + job_class = SolidQueue.recurring_task_job_class + job_class.is_a?(String) ? job_class.constantize : job_class end def enqueue_options diff --git a/lib/solid_queue.rb b/lib/solid_queue.rb index fbc2f5e6..47b96909 100644 --- a/lib/solid_queue.rb +++ b/lib/solid_queue.rb @@ -42,6 +42,7 @@ module SolidQueue mattr_accessor :preserve_finished_jobs, default: true mattr_accessor :clear_finished_jobs_after, default: 1.day mattr_accessor :default_concurrency_control_period, default: 3.minutes + mattr_accessor :recurring_task_job_class, default: "SolidQueue::RecurringJob" mattr_reader :time_zone diff --git a/test/models/solid_queue/recurring_task_test.rb b/test/models/solid_queue/recurring_task_test.rb index 518fc7ed..4c8b81ef 100644 --- a/test/models/solid_queue/recurring_task_test.rb +++ b/test/models/solid_queue/recurring_task_test.rb @@ -49,6 +49,9 @@ def perform end end + class CustomRecurringCommandJob < SolidQueue::RecurringJob + end + test "job without arguments" do task = recurring_task_with(class_name: "JobWithoutArguments") enqueue_and_assert_performed_with_result task, "job_without_arguments" @@ -222,6 +225,37 @@ def perform assert_equal "SolidQueue::RecurringJob", SolidQueue::Job.last.class_name end + test "task configured with a command uses a custom recurring task job class" do + SolidQueue.stubs(:recurring_task_job_class).returns(CustomRecurringCommandJob) + + task = recurring_task_with(command: "JobBuffer.add('from_a_command')") + enqueue_and_assert_performed_with_result(task, "from_a_command") + + assert_equal "SolidQueue::RecurringTaskTest::CustomRecurringCommandJob", SolidQueue::Job.last.class_name + end + + test "task configured with a command uses a custom recurring task job class given as a string" do + SolidQueue.stubs(:recurring_task_job_class).returns("SolidQueue::RecurringTaskTest::CustomRecurringCommandJob") + + task = recurring_task_with(command: "JobBuffer.add('from_a_command')") + enqueue_and_assert_performed_with_result(task, "from_a_command") + + assert_equal "SolidQueue::RecurringTaskTest::CustomRecurringCommandJob", SolidQueue::Job.last.class_name + end + + test "default_job_class is deprecated in favor of SolidQueue.recurring_task_job_class" do + SolidQueue.with(recurring_task_job_class: SolidQueue.recurring_task_job_class) do + assert_deprecated(SolidQueue.deprecator) do + SolidQueue::RecurringTask.default_job_class = "SolidQueue::RecurringTaskTest::CustomRecurringCommandJob" + end + assert_equal "SolidQueue::RecurringTaskTest::CustomRecurringCommandJob", SolidQueue.recurring_task_job_class + + assert_deprecated(SolidQueue.deprecator) do + assert_equal "SolidQueue::RecurringTaskTest::CustomRecurringCommandJob", SolidQueue::RecurringTask.default_job_class + end + end + end + test "schedule recurring tasks dynamically" do SolidQueue::RecurringTask.create_dynamic_task("test 1", command: "puts 1", schedule: "every hour") SolidQueue::RecurringTask.create_dynamic_task("test 2", command: "puts 2", schedule: "every minute", static: true)