diff --git a/Gemfile.lock b/Gemfile.lock index 2fdb3df54..0c537521e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,6 +15,7 @@ PATH activejob (>= 5.2.0) activerecord (>= 5.2.0) concurrent-ruby (>= 1.0.2) + fugit (>= 1.1) railties (>= 5.2.0) thor (>= 0.14.1) zeitwerk (>= 2.0) @@ -154,6 +155,8 @@ GEM rubocop smart_properties erubi (1.10.0) + et-orbi (1.2.4) + tzinfo faraday (1.5.1) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -177,6 +180,9 @@ GEM ffi (1.15.3-java) fiber-local (1.0.0) foreman (0.87.2) + fugit (1.5.0) + et-orbi (~> 1.1, >= 1.1.8) + raabro (~> 1.4) gem-release (2.2.2) github_changelog_generator (1.16.4) activesupport @@ -220,7 +226,6 @@ GEM mixlib-shellout (3.2.5) chef-utils msgpack (1.4.2) - msgpack (1.4.2-java) multi_json (1.15.0) multipart-post (2.1.1) nio4r (2.5.7) @@ -262,6 +267,7 @@ GEM nio4r (~> 2.0) puma (5.3.2-java) nio4r (~> 2.0) + raabro (1.4.0) racc (1.5.2) racc (1.5.2-java) rack (2.2.3) diff --git a/README.md b/README.md index 4b8d220f8..14ca4669b 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ For more of the story of GoodJob, read the [introductory blog post](https://isla - [Configuration options](#configuration-options) - [Global options](#global-options) - [Dashboard](#dashboard) - - [ActiveJob Concurrency](#activejob-concurrency) + - [ActiveJob concurrency](#activejob-concurrency) + - [Cron-style repeating/recurring jobs](#cron-style-repeatingrecurring-jobs) - [Updating](#updating) - [Go deeper](#go-deeper) - [Exceptions, retries, and reliability](#exceptions-retries-and-reliability) @@ -156,6 +157,7 @@ Options: [--poll-interval=SECONDS] # Interval between polls for available jobs in seconds (env var: GOOD_JOB_POLL_INTERVAL, default: 1) [--max-cache=COUNT] # Maximum number of scheduled jobs to cache in memory (env var: GOOD_JOB_MAX_CACHE, default: 10000) [--shutdown-timeout=SECONDS] # Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever)) + [--enable-cron] # Whether to run cron process (default: false) [--daemonize] # Run as a background daemon (default: false) [--pidfile=PIDFILE] # Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid) @@ -212,7 +214,8 @@ config.good_job.execution_mode = :async_server config.good_job.max_threads = 5 config.good_job.poll_interval = 30 # seconds config.good_job.shutdown_timeout = 25 # seconds - +config.good_job.enable_cron = true +config.good_job.cron = { example: { cron: '0 * * * *', class: 'ExampleJob' } } # ...or all at once. config.good_job = { @@ -220,6 +223,13 @@ config.good_job = { max_threads: 5, poll_interval: 30, shutdown_timeout: 25, + enable_cron: true, + cron: { + example: { + cron: '0 * * * *', + class: 'ExampleJob' + }, + }, } ``` @@ -235,6 +245,8 @@ Available configuration options are: - `poll_interval` (integer) sets the number of seconds between polls for jobs when `execution_mode` is set to `:async` or `:async_server`. You can also set this with the environment variable `GOOD_JOB_POLL_INTERVAL`. - `max_cache` (integer) sets the maximum number of scheduled jobs that will be stored in memory to reduce execution latency when also polling for scheduled jobs. Caching 10,000 scheduled jobs uses approximately 20MB of memory. You can also set this with the environment variable `GOOD_JOB_MAX_CACHE`. - `shutdown_timeout` (float) number of seconds to wait for jobs to finish when shutting down before stopping the thread. Defaults to forever: `-1`. You can also set this with the environment variable `GOOD_JOB_SHUTDOWN_TIMEOUT`. +- `enable_cron` (boolean) whether to run cron process. Defaults to `false`. You can also set this with the environment variable `GOOD_JOB_ENABLE_CRON`. +- `cron` (hash) cron configuration. Defaults to `{}`. You can also set this as a JSON string with the environment variable `GOOD_JOB_CRON` By default, GoodJob configures the following execution modes per environment: @@ -320,7 +332,7 @@ GoodJob includes a Dashboard as a mountable `Rails::Engine`. end ``` -### ActiveJob Concurrency +### ActiveJob concurrency GoodJob can extend ActiveJob to provide limits on concurrently running jobs, either at time of _enqueue_ or at _perform_. @@ -349,6 +361,38 @@ class MyJob < ApplicationJob end ``` +### Cron-style repeating/recurring jobs + +GoodJob can enqueue jobs on a recurring basis that can be used as a replacement for cron. + +Cron-style jobs are run on every GoodJob process (e.g. CLI or `async` execution mode) when `config.good_job.enable_cron = true`; use GoodJob's [ActiveJob concurrency](#activejob-concurrency) extension to limit the number of jobs that are enqueued. + +Cron-format is parsed by the [`fugit`](https://github.com/floraison/fugit) gem, which has support for seconds-level resolution (e.g. `* * * * * *`). + +```ruby +# config/environments/application.rb or a specific environment e.g. production.rb + +# Enable cron in this process; e.g. only run on the first Heroku worker process +config.good_job.enable_cron = ENV['DYNO'] == 'worker.1' # or `true` or via $GOOD_JOB_ENABLE_CRON + +# Configure cron with a hash that has a unique key for each recurring job +config.good_job.cron = { + # Every 15 minutes, enqueue `ExampleJob.set(priority: -10).perform_later(52, name: "Alice")` + frequent_task: { # each recurring job must have a unique key + cron: "*/15 * * * *", # cron-style scheduling format by fugit gem + class: "ExampleJob", # reference the Job class with a string + args: [42, { name: "Alice" }], # arguments to pass; can also be a proc e.g. `-> { { when: Time.now } }` + set: { priority: -10 }, # additional ActiveJob properties; can also be a lambda/proc e.g. `-> { { priority: [1,2].sample } }` + description: "Something helpful", # optional description that appears in Dashboard (coming soon!) + }, + another_task: { + cron: "0 0,12 * * *", + class: "AnotherJob", + }, + # etc. +} +``` + ### Updating GoodJob follows semantic versioning, though updates may be encouraged through deprecation warnings in minor versions. diff --git a/good_job.gemspec b/good_job.gemspec index fba1df74d..f36f776fe 100644 --- a/good_job.gemspec +++ b/good_job.gemspec @@ -51,6 +51,7 @@ Gem::Specification.new do |spec| spec.add_dependency "activejob", ">= 5.2.0" spec.add_dependency "activerecord", ">= 5.2.0" spec.add_dependency "concurrent-ruby", ">= 1.0.2" + spec.add_dependency "fugit", ">= 1.1" spec.add_dependency "railties", ">= 5.2.0" spec.add_dependency "thor", ">= 0.14.1" spec.add_dependency "zeitwerk", ">= 2.0" diff --git a/lib/good_job.rb b/lib/good_job.rb index c8c07dc96..c0389f7e8 100644 --- a/lib/good_job.rb +++ b/lib/good_job.rb @@ -106,16 +106,13 @@ def self.shutdown(timeout: -1, wait: nil) wait ? -1 : nil end - executables = Array(Notifier.instances) + Array(Poller.instances) + Array(Scheduler.instances) - _shutdown_all(executables, timeout: timeout) + _shutdown_all(_executables, timeout: timeout) end # Tests whether jobs have stopped executing. # @return [Boolean] whether background threads are shut down def self.shutdown? - Notifier.instances.all?(&:shutdown?) && - Poller.instances.all?(&:shutdown?) && - Scheduler.instances.all?(&:shutdown?) + _executables.all?(&:shutdown?) end # Stops and restarts executing jobs. @@ -126,8 +123,7 @@ def self.shutdown? # @param timeout [Numeric, nil] Seconds to wait for active threads to finish. # @return [void] def self.restart(timeout: -1) - executables = Array(Notifier.instances) + Array(Poller.instances) + Array(Scheduler.instances) - _shutdown_all(executables, :restart, timeout: timeout) + _shutdown_all(_executables, :restart, timeout: timeout) end # Sends +#shutdown+ or +#restart+ to executable objects ({GoodJob::Notifier}, {GoodJob::Poller}, {GoodJob::Scheduler}) @@ -146,5 +142,14 @@ def self._shutdown_all(executables, method_name = :shutdown, timeout: -1) end end + def self._executables + [].concat( + CronManager.instances, + Notifier.instances, + Poller.instances, + Scheduler.instances + ) + end + ActiveSupport.run_load_hooks(:good_job, self) end diff --git a/lib/good_job/adapter.rb b/lib/good_job/adapter.rb index 47e91ab35..241688e9d 100644 --- a/lib/good_job/adapter.rb +++ b/lib/good_job/adapter.rb @@ -57,6 +57,8 @@ def initialize(execution_mode: nil, queues: nil, max_threads: nil, poll_interval @scheduler = GoodJob::Scheduler.from_configuration(@configuration, warm_cache_on_initialize: Rails.application.initialized?) @notifier.recipients << [@scheduler, :create_thread] @poller.recipients << [@scheduler, :create_thread] + + @cron_manager = GoodJob::CronManager.new(@configuration.cron, start_on_initialize: Rails.application.initialized?) if @configuration.enable_cron? end end diff --git a/lib/good_job/cli.rb b/lib/good_job/cli.rb index 7474d0061..f7b49c11f 100644 --- a/lib/good_job/cli.rb +++ b/lib/good_job/cli.rb @@ -70,12 +70,16 @@ def exit_on_failure? type: :numeric, banner: 'SECONDS', desc: "Number of seconds to wait for jobs to finish when shutting down before stopping the thread. (env var: GOOD_JOB_SHUTDOWN_TIMEOUT, default: -1 (forever))" + method_option :enable_cron, + type: :boolean, + desc: "Whether to run cron process (default: false)" method_option :daemonize, type: :boolean, desc: "Run as a background daemon (default: false)" method_option :pidfile, type: :string, desc: "Path to write daemonized Process ID (env var: GOOD_JOB_PIDFILE, default: tmp/pids/good_job.pid)" + def start set_up_application! configuration = GoodJob::Configuration.new(options) @@ -87,7 +91,7 @@ def start scheduler = GoodJob::Scheduler.from_configuration(configuration, warm_cache_on_initialize: true) notifier.recipients << [scheduler, :create_thread] poller.recipients << [scheduler, :create_thread] - + cron_manager = GoodJob::CronManager.new(configuration.cron, start_on_initialize: true) if configuration.enable_cron? @stop_good_job_executable = false %w[INT TERM].each do |signal| trap(signal) { @stop_good_job_executable = true } @@ -98,7 +102,7 @@ def start break if @stop_good_job_executable || scheduler.shutdown? || notifier.shutdown? end - executors = [notifier, poller, scheduler] + executors = [notifier, poller, cron_manager, scheduler].compact GoodJob._shutdown_all(executors, timeout: configuration.shutdown_timeout) end @@ -124,6 +128,7 @@ def start type: :numeric, banner: 'SECONDS', desc: "Delete records finished more than this many seconds ago (env var: GOOD_JOB_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO, default: 86400)" + def cleanup_preserved_jobs set_up_application! diff --git a/lib/good_job/configuration.rb b/lib/good_job/configuration.rb index 8b57ce26f..74d63dd16 100644 --- a/lib/good_job/configuration.rb +++ b/lib/good_job/configuration.rb @@ -18,6 +18,8 @@ class Configuration DEFAULT_CLEANUP_PRESERVED_JOBS_BEFORE_SECONDS_AGO = 24 * 60 * 60 # Default to always wait for jobs to finish for {Adapter#shutdown} DEFAULT_SHUTDOWN_TIMEOUT = -1 + # Default to not running cron + DEFAULT_ENABLE_CRON = false # The options that were explicitly set when initializing +Configuration+. # @return [Hash] @@ -129,6 +131,28 @@ def shutdown_timeout ).to_f end + # Whether to run cron + # @return [Boolean] + def enable_cron + value = ActiveModel::Type::Boolean.new.cast( + options[:enable_cron] || + rails_config[:enable_cron] || + env['GOOD_JOB_ENABLE_CRON'] || + false + ) + value && cron.size.positive? + end + alias enable_cron? enable_cron + + def cron + env_cron = JSON.parse(ENV['GOOD_JOB_CRON']) if ENV['GOOD_JOB_CRON'].present? + + options[:cron] || + rails_config[:cron] || + env_cron || + {} + end + # Number of seconds to preserve jobs when using the +good_job cleanup_preserved_jobs+ CLI command. # This configuration is only used when {GoodJob.preserve_job_records} is +true+. # @return [Integer] diff --git a/lib/good_job/cron_manager.rb b/lib/good_job/cron_manager.rb new file mode 100644 index 000000000..6b5967f16 --- /dev/null +++ b/lib/good_job/cron_manager.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +require "concurrent/hash" +require "concurrent/scheduled_task" +require "fugit" + +module GoodJob # :nodoc: + # + # CronManagers enqueue jobs on a repeating schedule. + # + class CronManager + # @!attribute [r] instances + # @!scope class + # List of all instantiated CronManagers in the current process. + # @return [Array, nil] + cattr_reader :instances, default: [], instance_reader: false + + # Task observer for cron task + # @param time [Time] + # @param output [Object] + # @param thread_error [Exception] + def self.task_observer(time, output, thread_error) # rubocop:disable Lint/UnusedMethodArgument + return if thread_error.is_a? Concurrent::CancelledOperationError + + GoodJob.on_thread_error.call(thread_error) if thread_error && GoodJob.on_thread_error.respond_to?(:call) + end + + # Job configuration to be scheduled + # @return [Hash] + attr_reader :schedules + + # @param schedules [Hash] + # @param start_on_initialize [Boolean] + def initialize(schedules = {}, start_on_initialize: false) + @running = false + @schedules = schedules + @tasks = Concurrent::Hash.new + + self.class.instances << self + + start if start_on_initialize + end + + # Schedule tasks that will enqueue jobs based on their schedule + def start + ActiveSupport::Notifications.instrument("cron_manager_start.good_job", cron_jobs: @schedules) do + @running = true + schedules.each_key { |cron_key| create_task(cron_key) } + end + end + + # Stop/cancel any scheduled tasks + # @param timeout [Numeric, nil] Unused but retained for compatibility + def shutdown(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument + @running = false + @tasks.each do |_cron_key, task| + task.cancel + end + @tasks.clear + end + + # Stop and restart + # @param timeout [Numeric, nil] Unused but retained for compatibility + def restart(timeout: nil) # rubocop:disable Lint/UnusedMethodArgument + shutdown + start + end + + # Tests whether the manager is running. + # @return [Boolean, nil] + def running? + @running + end + + # Tests whether the manager is shutdown. + # @return [Boolean, nil] + def shutdown? + !running? + end + + # Enqueues a scheduled task + # @param cron_key [Symbol, String] the key within the schedule to use + def create_task(cron_key) + schedule = @schedules[cron_key] + return false if schedule.blank? + + fugit = Fugit::Cron.parse(schedule.fetch(:cron)) + delay = [(fugit.next_time - Time.current).to_f, 0].max + + future = Concurrent::ScheduledTask.new(delay, args: [self, cron_key]) do |thr_scheduler, thr_cron_key| + # Re-schedule the next cron task before executing the current task + thr_scheduler.create_task(thr_cron_key) + + CurrentExecution.reset + CurrentExecution.cron_key = thr_cron_key + + Rails.application.executor.wrap do + schedule = thr_scheduler.schedules.fetch(thr_cron_key).with_indifferent_access + job_class = schedule.fetch(:class).constantize + + job_set_value = schedule.fetch(:set, {}) + job_set = job_set_value.respond_to?(:call) ? job_set_value.call : job_set_value + + job_args_value = schedule.fetch(:args, []) + job_args = job_args_value.respond_to?(:call) ? job_args_value.call : job_args_value + + job_class.set(job_set).perform_later(*job_args) + end + end + + @tasks[cron_key] = future + future.add_observer(self.class, :task_observer) + future.execute + end + end +end diff --git a/lib/good_job/current_execution.rb b/lib/good_job/current_execution.rb index 0173ff83a..7a40d9c1d 100644 --- a/lib/good_job/current_execution.rb +++ b/lib/good_job/current_execution.rb @@ -11,6 +11,12 @@ module CurrentExecution # @return [String, nil] thread_mattr_accessor :active_job_id + # @!attribute [rw] cron_key + # @!scope class + # Cron Key + # @return [String, nil] + thread_mattr_accessor :cron_key + # @!attribute [rw] error_on_discard # @!scope class # Error captured by discard_on diff --git a/lib/good_job/job.rb b/lib/good_job/job.rb index 8359ce761..12dd5983f 100644 --- a/lib/good_job/job.rb +++ b/lib/good_job/job.rb @@ -199,6 +199,7 @@ def self.next_scheduled_at(after: nil, limit: 100, now_limit: nil) def self.enqueue(active_job, scheduled_at: nil, create_with_advisory_lock: false) ActiveSupport::Notifications.instrument("enqueue_job.good_job", { active_job: active_job, scheduled_at: scheduled_at, create_with_advisory_lock: create_with_advisory_lock }) do |instrument_payload| good_job_args = { + cron_key: CurrentExecution.cron_key, queue_name: active_job.queue_name.presence || DEFAULT_QUEUE_NAME, priority: active_job.priority || DEFAULT_PRIORITY, serialized_params: active_job.serialize, @@ -285,6 +286,25 @@ def active_job_id super || serialized_params['job_id'] end + def cron_key + if self.class.column_names.include?('cron_key') + super + else + ActiveSupport::Deprecation.warn(<<~DEPRECATION) + GoodJob has pending database migrations. To create the migration files, run: + + rails generate good_job:update + + To apply the migration files, run: + + rails db:migrate + + DEPRECATION + + nil + end + end + private # @return [ExecutionResult] @@ -295,6 +315,7 @@ def execute GoodJob::CurrentExecution.reset GoodJob::CurrentExecution.active_job_id = active_job_id + GoodJob::CurrentExecution.cron_key = cron_key ActiveSupport::Notifications.instrument("perform_job.good_job", { good_job: self, process_id: GoodJob::CurrentExecution.process_id, thread_name: GoodJob::CurrentExecution.thread_name }) do value = ActiveJob::Base.execute(params) diff --git a/lib/good_job/log_subscriber.rb b/lib/good_job/log_subscriber.rb index 1ece91ad2..76ef0d06a 100644 --- a/lib/good_job/log_subscriber.rb +++ b/lib/good_job/log_subscriber.rb @@ -57,6 +57,16 @@ def scheduler_create_pool(event) end end + # @!macro notification_responder + def cron_manager_start(event) + cron_jobs = event.payload[:cron_jobs] + cron_jobs_count = cron_jobs.size + + info do + "GoodJob started cron with #{cron_jobs_count} #{'jobs'.pluralize(cron_jobs_count)}." + end + end + # @!macro notification_responder def scheduler_shutdown_start(event) process_id = event.payload[:process_id] diff --git a/lib/good_job/railtie.rb b/lib/good_job/railtie.rb index 55a7f0e11..779658369 100644 --- a/lib/good_job/railtie.rb +++ b/lib/good_job/railtie.rb @@ -3,6 +3,7 @@ module GoodJob # Ruby on Rails integration. class Railtie < ::Rails::Railtie config.good_job = ActiveSupport::OrderedOptions.new + config.good_job.cron = {} initializer "good_job.logger" do |_app| ActiveSupport.on_load(:good_job) do @@ -23,6 +24,7 @@ class Railtie < ::Rails::Railtie config.after_initialize do GoodJob::Scheduler.instances.each(&:warm_cache) + GoodJob::CronManager.instances.each(&:start) end end end diff --git a/spec/integration/server_spec.rb b/spec/integration/server_spec.rb index b22e2f5c9..02b4fe06f 100644 --- a/spec/integration/server_spec.rb +++ b/spec/integration/server_spec.rb @@ -22,10 +22,12 @@ env = { "RAILS_ENV" => "production", "GOOD_JOB_EXECUTION_MODE" => "async", + "GOOD_JOB_ENABLE_CRON" => "true", } ShellOut.command('bundle exec rails s', env: env) do |shell| wait_until(max: 30) do expect(shell.output).to include(/GoodJob started scheduler/) + expect(shell.output).to include(/GoodJob started cron/) end end end diff --git a/spec/lib/good_job/cron_manager_spec.rb b/spec/lib/good_job/cron_manager_spec.rb new file mode 100644 index 000000000..dc6a3e602 --- /dev/null +++ b/spec/lib/good_job/cron_manager_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe GoodJob::CronManager do + let(:schedules) { {} } + + describe '#start' do + it 'stops the cron manager' do + cron_manager = described_class.new(schedules, start_on_initialize: false) + expect do + cron_manager.start + end.to change(cron_manager, :running?).from(false).to true + end + end + + describe '#stop' do + it 'starts the cron manager' do + cron_manager = described_class.new(schedules, start_on_initialize: true) + expect do + cron_manager.shutdown + end.to change(cron_manager, :running?).from(true).to false + end + end + + describe 'schedules' do + let(:schedules) do + { + example: { + cron: "* * * * * *", # cron-style scheduling format by fugit gem, allows seconds resolution + class: "TestJob", # reference the Job class with a string + args: [42, { name: "Alice" }], # arguments to pass. Could also allow a Proc for dynamic args, but problematic? + set: { priority: -10 }, # additional ActiveJob properties. Could also allow a Proc for dynamic args, but problematic? + description: "Something helpful", # optional description that appears in Dashboard + }, + } + end + + before do + stub_const 'TestJob', Class.new(ActiveJob::Base) + ActiveJob::Base.queue_adapter = GoodJob::Adapter.new(execution_mode: :external) + end + + it 'executes the defined tasks' do + cron_manager = described_class.new(schedules, start_on_initialize: true) + + wait_until(max: 5) do + expect(GoodJob::Job.count).to eq 3 + end + + good_job = GoodJob::Job.first + expect(good_job).to have_attributes( + cron_key: 'example', + priority: -10 + ) + + cron_manager.shutdown + end + end +end diff --git a/spec/support/reset_good_job.rb b/spec/support/reset_good_job.rb index b7f685365..407352a2a 100644 --- a/spec/support/reset_good_job.rb +++ b/spec/support/reset_good_job.rb @@ -32,6 +32,9 @@ expect(GoodJob::Poller.instances).to all be_shutdown GoodJob::Poller.instances.clear + expect(GoodJob::CronManager.instances).to all be_shutdown + GoodJob::CronManager.instances.clear + expect(GoodJob::Scheduler.instances).to all be_shutdown GoodJob::Scheduler.instances.clear diff --git a/spec/test_app/app/jobs/cleanup_job.rb b/spec/test_app/app/jobs/cleanup_job.rb new file mode 100644 index 000000000..bae2ef412 --- /dev/null +++ b/spec/test_app/app/jobs/cleanup_job.rb @@ -0,0 +1,6 @@ +class CleanupJob < ApplicationJob + def perform(limit: 5_000) + earliest = GoodJob::Job.finished.order(created_at: :desc).limit(limit).last.created_at + GoodJob::Job.where("created_at < ?", earliest).delete_all + end +end diff --git a/spec/test_app/config/application.rb b/spec/test_app/config/application.rb index 54aadaa40..789852def 100644 --- a/spec/test_app/config/application.rb +++ b/spec/test_app/config/application.rb @@ -19,6 +19,14 @@ class Application < Rails::Application # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks config.log_level = :debug + + config.good_job.cron = { + example: { + cron: '*/5 * * * * *', # every 5 seconds + class: 'ExampleJob', + description: "Enqueue ExampleJob every 5 seconds", + }, + } end end diff --git a/spec/test_app/config/environments/demo.rb b/spec/test_app/config/environments/demo.rb index a45095d6b..208a54215 100644 --- a/spec/test_app/config/environments/demo.rb +++ b/spec/test_app/config/environments/demo.rb @@ -7,4 +7,33 @@ config.active_job.queue_adapter = :good_job config.good_job.execution_mode = :async_server config.good_job.poll_interval = 30 + + config.good_job.enable_cron = true + config.good_job.cron = { + frequent_example: { + description: "Enqueue an ExampleJob with a random sample of configuration", + cron: "*/5 * * * * *", # every 5 seconds + class: "ExampleJob", + args: [], + set: (lambda do + queue = [:default, :elephants, :mice].sample + delay = (0..60).to_a.sample + priority = [-10, 0, 10].sample + + { wait: delay, queue: queue, priority: priority } + end), + }, + other_example: { + description: "Enqueue an OtherJob occasionally", + cron: "* * * * *", # every minute + class: "OtherJob", + set: { queue: :default }, + }, + cleanup: { + description: "Delete old jobs every hour", + cron: "0 * * * *", # every hour + class: "CleanupJob", + set: { queue: :default }, + } + } end