Simple Scheduler

Simple Scheduler is a scheduling add-on that is designed to be used with
Sidekiq and
Heroku Scheduler. It
gives you the ability to schedule tasks at any interval without adding
a clock process. Heroku Scheduler only allows you to schedule tasks every 10 minutes,
every hour, or every day.
Production Ready?
Yes. We are using Simple Scheduler in production for scheduling tasks that run
hourly, nightly, weekly, and every 10 minutes in Simple In/Out.
Why did we need to create yet another job scheduler?
Every option we evaluated seems to have the same flaw: If your server is down, your job won't run.
Check out our intro blog post to learn more.
Requirements
You must be using:
Both Active Job and Sidekiq::Worker classes can be queued by the scheduler.
* Not actually required, but you're on your own for scheduling the rake simple_scheduler
task to run every 10 minutes.
Installation
Add this line to your application's Gemfile:
gem "simple_scheduler"
And then execute:
$ bundle
Getting Started
Create the Configuration File
Create the file config/simple_scheduler.yml
in your Rails project:
queue_ahead: 360
queue_name: "default"
tz: "America/Chicago"
simple_task:
class: "SomeActiveJob"
every: "2.minutes"
at: "*:00"
overnight_task:
class: "SomeSidekiqWorker"
every: "1.day"
at: "4:00"
expires_after: "23.hours"
half_hour_task:
class: "HalfHourTask"
every: "30.minutes"
at: "*:30"
weekly_task:
class: "WeeklyJob"
every: "1.week"
at: "Sat 0:00"
tz: "America/New_York"
Set up Heroku Scheduler
Add the rake task to Heroku Scheduler and set it to run every 10 minutes:
rake simple_scheduler

It may be useful to point to a specific configuration file in non-production environments.
Use a custom configuration file by setting the SIMPLE_SCHEDULER_CONFIG
environment variable.
SIMPLE_SCHEDULER_CONFIG=config/simple_scheduler.staging.yml
Task Options
:class
The class name of the ActiveJob or Sidekiq::Worker. Your job or
worker class should accept the expected run time as a parameter
on the perform
method.
:every
How frequently the task should be performed as an ActiveSupport duration definition.
"1.day"
"5.days"
"12.hours"
"20.minutes"
"1.week"
:at
This is the starting point* for the :every
duration. This must be set so the expected
run times in the future can be determined without duplicating jobs already in the queue.
Valid string formats/examples:
"18:00"
"3:30"
"**:00"
"*:30"
"Sun 2:00"
* If you specify the hour of the day the job should run, it will only run in that hour,
so if you specify an :every
duration of less than 1.day
, the job will only run
when the run time hour matches the :at
time's hour.
See #17 for more info.
:expires_after (optional)
If your worker process is down for an extended period of time, you may not want certain jobs
to execute when the server comes back online. The :expires_after
value will be used
to determine if it's too late to run the job at the actual run time.
All jobs are scheduled in the future using the SimpleScheduler::FutureJob
. This
wrapper job does the work of evaluating the current time and determining if the
scheduled job should run. See Handling Expired Jobs.
The string should be in the form of an ActiveSupport duration.
"59.minutes"
"23.hours"
:queue_ahead (optional)
The :queue_ahead
is the number of minutes that the job should be scheduled into the future.
The default value is 360
, so Simple Scheduler will make sure you have scheduled jobs for
the next 6 hours. This allows for a major outage without losing track of jobs that were
supposed to run during that time.
There are always a minimum of 2 scheduled jobs for each scheduled task. This ensures there
is always one job in the queue that can be used to determine the next run time, even if one of
the two was executed during the 10 minute Heroku Scheduler wait time.
The :queue_ahead
can be set as a global configuration option or for each individual task.
:tz (optional)
Use :tz
to specify the time zone of your :at
time. If no time zone is specified, the Time.zone
default in your Rails app will be used when parsing the :at
time. A list of all the available
timezone identifiers can be obtained using TZInfo::Timezone.all_identifiers
.
The :tz
can be set as a global configuration option or for each individual task.
Writing Your Jobs
There is no guarantee that the job will run at the exact time given in the
configuration, so the time the job was scheduled to run will be passed to
the job. This allows you to handle situations where the current time doesn't
match the time it was expected to run. The scheduled_time
argument is optional.
class ExampleJob < ActiveJob::Base
def perform(scheduled_time)
puts Time.at(scheduled_time)
end
end
Determine if the Job Should Run
Simple Scheduler doesn't support specifying multiple days of the week or a specific day of the month.
You can use the scheduled_time
with a guard clause to ensure the job only runs on specific days.
Set up the jobs to run every day, even though we don't actually want them to run every day:
payroll:
class: "PayrollJob"
every: "1.day"
at: "0:00"
weekday_reports:
class: "WeekdayReportsJob"
every: "1.day"
at: "18:00"
Use a guard clause in each of our jobs to exit the job if it shouldn't run that day:
class PayrollJob < ActiveJob::Base
def perform(scheduled_time)
day_of_the_month = Time.at(scheduled_time).day
return unless day_of_the_month == 1 || day_of_the_month == 15
end
end
class WeekdayReportsJob < ActiveJob::Base
def perform(scheduled_time)
day_of_the_week = Time.at(scheduled_time).wday
return unless (1..5).include?(day_of_the_week)
end
end
Handling Expired Jobs
If you assign the :expires_after
option to your task, you may want to know if
a job wasn't run because it expires. Add this block to an initializer file:
SimpleScheduler.expired_task do |exception|
ExceptionNotifier.notify_exception(
exception,
data: {
task: exception.task.name,
scheduled: exception.scheduled_time,
actual: exception.run_time
}
)
end
Making Changes to Configuration File
Any changes made to the YAML configuration file will be picked up by Simple Scheduler
the next time rake simple_scheduler
is run. Depending on the changes, you may need
to reset the current job queue.
Reasons you may need to reset the job queue:
- Renaming a job's class
- Deleting a scheduled task
- Changing the task's run time interval
- Changing the day of the week the job is run
- Changing the
queue_name
in the config file
A rake task exists to delete all existing scheduled jobs and queue them back up from scratch:
rake simple_scheduler:reset
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
)
- Commit your changes (
git commit -am 'Add some feature'
)
- Push to the branch (
git push origin my-new-feature
)
- Create new Pull Request
License
The gem is available as open source under the terms of the MIT License.