A scheduling add-on for Sidekiq
🎬 Introduction video about Sidekiq-Cron by Drifting Ruby
Sidekiq-Cron runs a thread alongside Sidekiq workers to schedule jobs at specified times (using cron notation * * * * *
or natural language, powered by Fugit).
Checks for new jobs to schedule every 30 seconds and doesn't schedule the same job multiple times when more than one Sidekiq process is running.
Scheduling jobs are added only when at least one Sidekiq process is running, but it is safe to use Sidekiq-Cron in environments where multiple Sidekiq processes or nodes are running.
If you want to know how scheduling work, check out under the hood.
Changelog
Before upgrading to a new version, please read our Changelog.
Installation
Install the gem:
$ gem install sidekiq-cron
Or add to your Gemfile
and run bundle install
:
gem "sidekiq-cron"
NOTE If you are not using Rails, you need to add require 'sidekiq-cron'
somewhere after require 'sidekiq'
.
Getting Started
Job properties
{
'name' => 'name_of_job',
'cron' => '1 * * * *',
'class' => 'MyClass',
'namespace' => 'YourNamespace',
'source' => 'dynamic',
'queue' => 'name of queue',
'retry' => '5',
'args' => '[Array or Hash] of arguments which will be passed to perform method',
'date_as_argument' => true,
'active_job' => true,
'queue_name_prefix' => 'prefix',
'queue_name_delimiter' => '.',
'description' => 'A sentence describing what work this job performs'
'status' => 'disabled'
}
NOTE The status
of a job does not get changed in Redis when a job gets reloaded unless the status
property is explicitly set.
Configuration
All configuration options:
Sidekiq::Cron.configure do |config|
config.cron_poll_interval = 10
config.cron_schedule_file = 'config/my_schedule.yml'
config.cron_history_size = 20
config.default_namespace = 'statistics'
config.natural_cron_parsing_mode = :strict
config.reschedule_grace_period = 300
end
If you are using Rails, you should add the above block inside an initializer (config/initializers/sidekiq-cron.rb
).
Time, cron and Sidekiq-Cron
For testing your cron notation you can use crontab.guru.
Sidekiq-Cron uses Fugit to parse the cronline. So please, check Fugit documentation for further information about allowed formats.
If using Rails, this is evaluated against the timezone configured in Rails, otherwise the default is UTC.
If you want to have your jobs enqueued based on a different time zone you can specify a timezone in the cronline,
like this '0 22 * * 1-5 America/Chicago'
.
Natural-language formats
Since Sidekiq-Cron v1.7.0
, you can use the natural-language formats supported by Fugit, such as:
"every day at five"
"every 3 hours"
See the relevant part of Fugit documentation for details.
There are multiple modes that determine how natural-language cron strings will be parsed.
:single
(default)
Sidekiq::Cron.configure do |config|
config.natural_cron_parsing_mode = :single
end
This parses the first possible cron line from the given string and then ignores any additional cron lines.
Ex. every day at 3:15 and 4:30
- Equivalent to
15 3 * * *
. 30 4 * * *
gets ignored.
:strict
Sidekiq::Cron.configure do |config|
config.natural_cron_parsing_mode = :strict
end
This throws an error if the given string would be parsed into multiple cron lines.
Ex. every day at 3:15 and 4:30
- Would throw an error and the associated cron job would be invalid
Second-precision (sub-minute) cronlines
In addition to the standard 5-parameter cronline format, Sidekiq-Cron supports scheduling jobs with second-precision using a modified 6-parameter cronline format:
Seconds Minutes Hours Days Months DayOfWeek
For example: "*/30 * * * * *"
would schedule a job to run every 30 seconds.
Note that if you plan to schedule jobs with second precision you may need to override the default schedule poll interval so it is lower than the interval of your jobs:
Sidekiq::Cron.configure do |config|
config.cron_poll_interval = 10
end
The default value at time of writing is 30 seconds. See under the hood for more details.
Namespacing
Default namespace
When not giving a namespace, the default
one will be used.
In the case you'd like to change this value, you can change it via the following configuration flag:
Sidekiq::Cron.configure do |config|
config.default_namespace = 'statics'
end
Usage
When creating a new job, you can optionally give a namespace
attribute, and then you can pass it too in the find
or destroy
methods.
Sidekiq::Cron::Job.create(
name: 'Hard worker - every 5min',
namespace: 'Foo',
cron: '*/5 * * * *',
class: 'HardWorker'
)
Sidekiq::Cron::Job.count
Sidekiq::Cron::Job.count 'Foo'
Sidekiq::Cron::Job.all
Sidekiq::Cron::Job.all 'Foo'
Sidekiq::Cron::Job.all '*'
job = Sidekiq::Cron::Job.find('Hard worker - every 5min', 'Foo').first
job.destroy
What objects/classes can be scheduled
Sidekiq Worker
In this example, we are using HardWorker
which looks like:
class HardWorker
include Sidekiq::Worker
def perform(*args)
end
end
For Sidekiq workers, symbolize_args: true
in Sidekiq::Cron::Job.create
or in Hash configuration is gonna be ignored as Sidekiq currently only allows for simple JSON datatypes.
Active Job
You can schedule ExampleJob
which looks like:
class ExampleJob < ActiveJob::Base
queue_as :default
def perform(*args)
end
end
For Active Job you can use symbolize_args: true
in Sidekiq::Cron::Job.create
or in Hash configuration,
which will ensure that arguments you are passing to it will be symbolized when passed back to perform
method in worker.
Adding Cron jobs
Refer to Schedule vs Dynamic jobs to understand the difference.
class HardWorker
include Sidekiq::Worker
def perform(name, count)
end
end
Sidekiq::Cron::Job.create(name: 'Hard worker - every 5min', cron: '*/5 * * * *', class: 'HardWorker')
create
method will return only true/false if job was saved or not.
job = Sidekiq::Cron::Job.new(name: 'Hard worker - every 5min', cron: '*/5 * * * *', class: 'HardWorker')
if job.valid?
job.save
else
puts job.errors
end
unless job.save
puts job.errors
end
Use ActiveRecord models as arguments:
class Person < ApplicationRecord
end
class HardWorker < ActiveJob::Base
queue_as :default
def perform(person)
puts "person: #{person}"
end
end
person = Person.create(id: 1)
Sidekiq::Cron::Job.create(name: 'Hard worker - every 5min', cron: '*/5 * * * *', class: 'HardWorker', args: person)
Load more jobs from hash:
hash = {
'name_of_job' => {
'class' => 'MyClass',
'cron' => '1 * * * *',
'args' => '(OPTIONAL) [Array or Hash]'
},
'My super iber cool job' => {
'class' => 'SecondClass',
'cron' => '*/5 * * * *'
}
}
Sidekiq::Cron::Job.load_from_hash hash
Load more jobs from array:
array = [
{
'name' => 'name_of_job',
'class' => 'MyClass',
'cron' => '1 * * * *',
'args' => '(OPTIONAL) [Array or Hash]'
},
{
'name' => 'Cool Job for Second Class',
'class' => 'SecondClass',
'cron' => '*/5 * * * *'
}
]
Sidekiq::Cron::Job.load_from_array array
Bang-suffixed methods will remove jobs where source is schedule
and are not present in the given hash/array, update jobs that have the same names, and create new ones when the names are previously unknown.
Sidekiq::Cron::Job.load_from_hash! hash
Sidekiq::Cron::Job.load_from_array! array
Loading jobs from schedule file
You can also load multiple jobs from a YAML file:
my_first_job:
cron: "*/5 * * * *"
class: "HardWorker"
queue: hard_worker
second_job:
cron: "*/30 * * * *"
class: "HardWorker"
queue: hard_worker_long
args:
hard: "stuff"
There are multiple ways to load the jobs from a YAML file
-
The gem will automatically load the jobs mentioned in config/schedule.yml
file (it supports ERB)
-
When you want to load jobs from a different filename, mention the filename in Sidekiq configuration as follows:
Sidekiq::Cron.configure do |config|
config.cron_schedule_file = "config/users_schedule.yml"
end
-
Load the file manually as follows:
Sidekiq.configure_server do |config|
config.on(:startup) do
schedule_file = "config/users_schedule.yml"
if File.exist?(schedule_file)
schedule = YAML.load_file(schedule_file)
Sidekiq::Cron::Job.load_from_hash!(schedule, source: "schedule")
end
end
end
Finding jobs
Sidekiq::Cron::Job.all
Sidekiq::Cron::Job.find "Job Name"
Sidekiq::Cron::Job.find name: "Job Name"
Destroy jobs
Sidekiq::Cron::Job.destroy_all!
Sidekiq::Cron::Job.destroy "Job Name"
Sidekiq::Cron::Job.find('Job name').destroy
Work with job
job = Sidekiq::Cron::Job.find('Job name')
job.disable!
job.enable!
job.status
job.enqueue!
Schedule vs Dynamic jobs
There are two potential job sources: schedule
and dynamic
.
Jobs associated with schedule files are labeled as schedule
as their source,
whereas jobs created at runtime without the source=schedule
argument are classified as dynamic
.
The key distinction lies in how these jobs are managed.
When a schedule is loaded, any stale schedule
jobs are automatically removed to ensure synchronization within the schedule.
The dynamic
jobs remain unaffected by this process.
How to start scheduling?
Just start Sidekiq workers by running:
$ bundle exec sidekiq
Web UI for Cron Jobs
If you are using Sidekiq's web UI and you would like to add cron jobs too to this web UI,
add require 'sidekiq/cron/web'
after require 'sidekiq/web'
.
With this, you will get:
Under the hood
When you start the Sidekiq process, it starts one thread with Sidekiq::Poller
instance, which perform the adding of scheduled jobs to queues, retries etc.
Sidekiq-Cron adds itself into this start procedure and starts another thread with Sidekiq::Cron::Poller
which checks all enabled Sidekiq cron jobs every 30 seconds, if they should be added to queue (their cronline matches time of check).
Sidekiq-Cron is checking jobs to be enqueued every 30s by default, you can change it by setting:
Sidekiq::Cron.configure do |config|
config.cron_poll_interval = 10
end
When Sidekiq (and Sidekiq-Cron) is not used in zero-downtime deployments, after the deployment is done Sidekiq-Cron starts to catch up. It will consider older jobs that missed their schedules during that time. By default, only jobs that should have started less than 1 minute ago are considered. This is problematic for some jobs, e.g., jobs that run once a day. If on average Sidekiq is shut down for 10 minutes during deployments, you can configure Sidekiq-Cron to consider jobs that were about to be scheduled during that time:
Sidekiq::Cron.configure do |config|
config.reschedule_grace_period = 600
end
Sidekiq-Cron is safe to use with multiple Sidekiq processes or nodes. It uses a Redis sorted set to determine that only the first process who asks can enqueue scheduled jobs into the queue.
When running with many Sidekiq processes, the polling can add significant load to Redis. You can disable polling on some processes by setting:
Sidekiq::Cron.configure do |config|
config.cron_poll_interval = 0
end
Contributing
Thanks to all contributors, you’re awesome and this wouldn’t be possible without you!
- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
- Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
- Fork the project.
- Start a feature/bugfix branch.
- Commit and push until you are happy with your contribution.
- Make sure to add tests for it. This is important so we don't break it in a future version unintentionally.
- Open a pull request!
Testing
You can execute the test suite by running:
$ bundle exec rake test
Using Docker
This project uses Docker Compose in order to orchestrate containers and get the test suite running on you local machine, and here you find the commands to run in order to get a complete environment to build and test this gem:
- Build the Docker image (only the first time):
docker compose -f docker/docker-compose.yml build
- Run the test suite:
docker compose -f docker/docker-compose.yml run --rm tests
This command will download the first time the project's dependencies (Redis so far), create the containers and run the default command to run the tests.
Running other commands
In the case you need to run a command in the gem's container, you would do it like so:
docker compose -f docker/docker-compose.yml run --rm tests <HERE IS YOUR COMMAND>
Note that tests
is the Docker Compose service name defined in the docker/docker-compose.yml
file.
Running a single test file
Given you only want to run the tests from the test/unit/web_extension_test.rb
file, you need to pass its path with the TEST
env variable, so here is the command:
docker compose -f docker/docker-compose.yml run --rm --env TEST=test/unit/web_extension_test.rb tests
License
Copyright (c) 2013 Ondrej Bartas. See LICENSE for further details.