#164 Cron in Ruby (revised)
- Download:
- source codeProject Files in Zip (78.3 KB)
- mp4Full Size H.264 Video (20.1 MB)
- m4vSmaller H.264 Video (9.84 MB)
- webmFull Size VP8 Video (7.52 MB)
- ogvFull Size Theora Video (27 MB)
Cron is one of the most common ways to handle scheduled, recurring jobs but its syntax can be quite cryptic. Here’s an example from the Wikipedia page on cron.
1 0 * * * printf > /www/apache/logs/error_log
It’s not obvious how often this command runs. Is it every minute, on the first minute of every hour or something else? It will actually run on the first minute of each day. What about this one?
0 */2 * * * /home/username/test.pl
This one runs every two hours but understanding this and getting the syntax right in your own cron jobs can be tricky. Cron jobs are often specific to a single application so it would be nice to manage them within an application instead of directly on the server. This is where the Whenever gem comes in. This gives us a convenient way to manage an application’s cron tasks directly in Ruby. It gives us a nice syntax for defining how often a given task should run and some convenient commands for running different types of task.
Creating Scheduled Tasks With Whenever
To add Whenever to an application we need to add it to the gemfile and then run bundle to install it.
source 'http://rubygems.org' gem 'rails', '3.1.3' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.5' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'whenever', require: false
Note that we’ve used the require
option and set it to false
because we don’t want to use Whenever in the Rails application. We set up Whenever in our application by running the whenverize
command, though we should run it with bundle exec
.
$ bundle exec wheneverize [add] writing `./config/schedule.rb' [done] wheneverized!
This command creates a schedule.rb
file in the /config
directory with some documentation in it. We’ll replace this with our own tasks. Let’s say that we want to remove a cache file every fifteen minutes. We write that task like this:
every 15.minutes do command "rm '#{path}/tmp/cache/foo.txt'" end
The command
method runs a terminal command and the we can use the path
variable to give us access to our application’s path. We can see the equivalent cron code at any time by running whenever, again through bundle exec
.
$ bundle exec whenever 0,15,30,45 * * * * /bin/bash -l -c 'rm '\''/Users/eifion/schedules/tmp/cache/foo.txt'\'''
The output from this command shows the cron equivalent for our task.
There’s a variety of other ways to specify schedules with Whenever. For example if we want to run some Ruby code rather than a terminal command we can do so. Let’s say that we have an e-commerce application and we want to clear out any abandoned carts once a week, early on a Sunday morning. We have a class method on our Cart
model for doing this and we can call runner
to run it.
every :sunday, at: "4:28 AM" do runner "Cart.clear_abandoned" end
This command will load up the Rails environment and run the Ruby code we’ve passed to it. We can also run Rake tasks this way. If our application has a Thinking Sphinx server we’ll want to be sure that it’s started up each time the server is rebooted. We can do that by adding this task:
every :reboot do rake "ts:start" end
If we run whenever
now we’ll see each of these tasks translated to the equivalent cron syntax.
$ bundle exec whenever @reboot /bin/bash -l -c 'cd /Users/eifion/schedules && RAILS_ENV=production bundle exec rake ts:start --silent' 0,15,30,45 * * * * /bin/bash -l -c 'rm '\''/Users/eifion/schedules/tmp/cache/foo.txt'\''' 28 4 * * 0 /bin/bash -l -c 'cd /Users/eifion/schedules && script/rails runner -e production '\''Cart.clear_abandoned'\'''
Whenever automatically handles running Ruby code through rails runner
and running the Rake tasks correctly so we don’t need to worry about any of this.
It’s a good idea to keep a log of the output from these commands and we can do this with the set command, using the output option and specifying a path.
set :output, "#{path}/log/cron.log" every 15.minutes do command "rm '#{path}/tmp/cache/foo.txt'" end every :sunday, at: "4:28 AM" do runner "Cart.clear_abandoned" end every :reboot do rake "ts:start" end
This will log output to a cron.log
file in the application’s log
directory and if we run whenever again we’ll see that the commands now pipe their output to that log file.
$ bundle exec whenever @reboot /bin/bash -l -c 'cd /Users/eifion/schedules && RAILS_ENV=production bundle exec rake ts:start --silent >> /Users/eifion/schedules/log/cron.log 2>&1' 0,15,30,45 * * * * /bin/bash -l -c 'rm '\''/Users/eifion/schedules/tmp/cache/foo.txt'\'' >> /Users/eifion/schedules/log/cron.log 2>&1' 28 4 * * 0 /bin/bash -l -c 'cd /Users/eifion/schedules && script/rails runner -e production '\''Cart.clear_abandoned'\'' >> /Users/eifion/schedules/log/cron.log 2>&1'
Creating Our Own Job Types
We can even create our own job types. Whenever comes with three: command
, runner
and rake
but if we run something else frequently we can make our own job type. If, for example, we have a lot of scripts in our application’s script
directory that we run through cron we can make a new job type to run them.
set :output, "#{path}/log/cron.log" job_type :script, "'#{path}/script/:task :output" every 15.minutes do command "rm '#{path}/tmp/cache/foo.txt'" script "generate_report" end
We use job_type
to create a new job type and give it a name and a path. For our job this the path is application’s script
directory and :task
indicates that it should run whatever task we pass in. We’ll also use :output
so that any output the script returns is passed to the log file. With this in place we can non define script
jobs just like any other type of job and when we run whenever again we’ll see our custom job in its output.
$ bundle exec whenever @reboot /bin/bash -l -c 'cd /Users/eifion/schedules && RAILS_ENV=production bundle exec rake ts:start --silent >> /Users/eifion/schedules/log/cron.log 2>&1' 0,15,30,45 * * * * /bin/bash -l -c 'rm '\''/Users/eifion/schedules/tmp/cache/foo.txt'\'' >> /Users/eifion/schedules/log/cron.log 2>&1' 0,15,30,45 * * * * /bin/bash -l -c ''\''/Users/eifion/schedules/script/generate_report >> /Users/eifion/schedules/log/cron.log 2>&1' 28 4 * * 0 /bin/bash -l -c 'cd /Users/eifion/schedules && script/rails runner -e production '\''Cart.clear_abandoned'\'' >> /Users/eifion/schedules/log/cron.log 2>&1'
Deploying Tasks
We now have our schedules set up the way we want but how do we deploy them to the server? If we’re using Capistrano this is easy, all we need to do is add two lines to the deploy.rb
file.
require "bundler/capistrano" set :whenever_command, "bundle exec whenever" require 'whenever/capistrano'
The middle line above tells Capistrano to execute the whenever
command through Bundler and the line below adds a Capistrano task to write to the crontab every time the application is deployed. That’s all we need to do. The next time we deploy the application everything will be set up automatically for us.
Alternatives
Whenever is a great way to manage cron jobs from inside a Rails application, but cron isn’t the best solution for every kind of scheduled job. If a job needs to run very frequently or for long running tasks then it’s worth considering some alternatives.
If you’re using Redis and Resque to manage a job queue (these were covered in episode 271) take a look at Resque Scheduler. This is a persistent process that handles jobs on a schedule using Redis and Resque. A nice advantage of this is that we can see the status of our jobs through the Web UI that Resque provides. Resque Scheduler is built on top of a gem called Rufus Scheduler. If you’re not using Resque and you need a more generic solution then this may provide a better solution. Another gem worth look at is Clockwork. This works with a variety of queuing engines and provides a nice syntax similar to Whenever’s.