ScheduledJob
Scheduled job looks to build on top of the delayed_job project by adding support for jobs that need to recur. Whilst investigating other options we decided that we wanted a very light weight framework that would simply allow us to define worker tasks that need to happen on a regular basis.
In order to achieve this we created the following interface which allows the developer to consisly define what the job is to do as well as when it is to run. This helps keep all the logic in one place which is a huge plus.
In terms of implementation there are only a couple of things we need to do.
Firstly if there are any before or success callbacks you need to define you can do this via the configure block. This passes the instance of DelayedJob that run your job as well as the job itself.
We can also take this opportunity to set up any logging. By default we use the ruby logger but if you are using rails for example you can do something like the following:
ScheduledJob.configure do |config|
config.before_callback = -> (job, scheduled_job) do
JobRunLogger.update_attributes!(job_name: scheduled_job.class.name, started_at: Time.now.utc)
end
config.success_callback = -> (job, _) do
ScheduledJob.logger.info("Hurrah my job #{job.id} has completed")
end
config.logger = Rails.logger
end
With this in place we can go on to define a job that we want to run regularly. To do this just mix in the scheduled job module in your class, define a perform method and define a time to recur.
class WeeklyMetricJob
include ::ScheduledJob
def perform
ScheduledJob.logger.info('I need to do something over and over')
end
def self.time_to_recur(last_run_at)
last_run_at.end_of_week + 3.hours
end
end
This allows you so specify what logic you need to run along side how often it needs to run. The time to recur is passed the completion time of the last successful run so you can use whatever logic you like in here to define when the job needs to run again.
Finally you need to kick off the job the first time. Once it has run successfully it will look after itself but to start the cycle you need to run schedule_job on your job. Continuing on the example above:
WeeklyMetricJob.schedule_job
Note currently this implementation is dependant upon using the delayed_job_active_record backend. This is something that we may be looking to remove in future.
Running the specs
This is the default rake task so you can run the specs in any of the following ways:
bundle exec rake
bundle exec rake spec
bundle exec rspec
Getting a console
The project is currently using pry. In order to get a console in the context of the project just run the pry.rb file in ruby.
bundle exec ruby pry.rb
Installation
Add this line to your application's Gemfile:
gem 'scheduled_job'
And then execute:
$ bundle
Or install it yourself as:
$ gem install scheduled_job
Usage
First you must include the scheduled job module in any DelayedJob that needs to run on a regular basis.
include ::ScheduledJob
Then you need to say what the job is actually to do. This is done by implementing the perform method.
def perform
puts 'I do work!'
end
Finally we need to write the logic for when we want the job to run. This is done by implementing the time_to_recur method which is passed the time the job last completed as its parameter.
def self.time_to_recur(last_run_at)
last_run_at + 3.hours
end
Recently added is the new jobs configuration. This adds two major new benefits. Firstly this will allow you to define jobs that are allow to run in multiple instances. Say for example that there should always be two instances of a given job running. This can now be defined using the following configuration:
ScheduledJob.configure do |config|
config.jobs = {
MyAwesomeParallelJobClass => { count: 2 }
}
end
This lets scheduled job know that it is OK to have two pending job instances for MyAwesomeParallelJobClass in the delayed job table. Additionally by using this configuration you also get access to the new reschedule rake task for free. ScheduledJob now adds rake jobs:reschedule
. This will loop through your jobs configuration and automatically call schedule job up to the number of times you intend the jobs pending. This is useful for heavy users of scheduled job to "prime" your database with your recurring jobs. Note you can still add jobs to this configuration that you do not want any instances of by setting the count to 0. This is useful if you are looking to access all classes you have that you have registered with scheduled job.
There are also callbacks that are available using ScheduledJob. These allow you to hook into the scheduling life cycle. Also note that as this uses DelayedJob under the hood all of the delayed job callbacks are still available for use.
These can be defined when configuring the gem for you application on the configure block:
ScheduledJob.configure do |config|
# configuration code in here
end
The before_callback is executed before the perform method is called on the scheduled job. This is passed the delayed job object and the scheduled job instance.
config.before_callback = -> (job, scheduled_job) do
JobRunLogger.update_attributes!(job_name: scheduled_job.class.name, started_at: Time.now.utc)
end
The success_callback is called on successful completion of the job and is also passed the delayed job object and the scheduled job instance.
config.success_callback = -> (job, _) do
ScheduledJob.logger.info("Hurrah my job #{job.id} has completed")
end
Then there is the fast mode. This is checked prior to scheduling another run of your job e.g. after a job has completed. This allows you to override the scheduling logic and ask the job to run immediately. This is passed the scheduled job class. This means you can have state stored elsewhere to change the scheduling without having to modify the code. This could be getting an array from a database for example:
config.fast_mode = -> (job) do
Database.get_value('fast_mode_jobs').include?(job.name)
end
Contributing
- Fork it ( https://github.com/rightscale/scheduled_job/fork )
- 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