homeASCIIcasts

164: Cron in Ruby 

(view original Railscast)

In your Rails applications you’ll often need to handle recurring processes or tasks, be it rolling log files once an hour, restarting a daemon once a week or clearing out old cache files. The most common way of doing this is to use cron, which is available on most UNIX-based systems.

Say you want to run a rake task under cron every morning. You’d start by editing a new crontab file by running

crontab -e

which would bring up an empty crontab file for editing. Unless you’re familiar with the syntax for crontab you’ll probably start reaching for the manuals about now as it can be a little tricky to remember. For example, if you wanted to run a command at 2.30 every morning you’d have to enter something like

30 2 * * * cd /Users/eifion/rails/myrailsapp && execute_some_command

and then test it to make sure you’d got the syntax correct. Another problem with using crontab is that every time you want to make a change you have to log on to the production server. Also, by using cron you’re creating an external dependency and have to remember to deploy the cron files when deploying your application.

Given all of this, it would be great if there was a solution that did all of this automatically. And fortunately there is, in a Ruby gem called Whenever which allows us to write cron jobs in Ruby using a much simpler syntax.

every 3.hours do
  runner "MyModel.some_process"
  rake "my:rake:task"
  command "/usr/bin/my_great_command"
end

An Example of a Whenever Task

The code above will create a job that will run every three hours. In each job we can run Ruby code with script/runner, execute rake tasks or run any external command that we choose. Whenever will also automatically handle the behind-the-scenes work, moving to your Rails app’s directory, running the rake tasks in production mode and so on.

Whenever has the advantage that it ties your cron jobs into your Rails application. If you have multiple Rails applications on a server then you can keep each app’s jobs separate. Also if you deploy an application to a new server then you don’t have to remember to manually install crontabs.

Installing Whenever

We can install Whenever by adding the gem to our application’s /config/environment.rb file.

config.gem 'javan-whenever', :lib => false, :source => 'http://gems.github.com'

This line isn’t required, but by adding it we’re adding a dependency to the gem in our application so that it gets installed when we deploy it. Next, we’ll make sure that the gems has been installed.

sudo rake gems:install

Now that the Whenever gem is installed we can run the wheneverize command to set it up. We have to pass it the path to the Rails application, which will just be “.” if you’re in your application’s directory.

$ wheneverize .
[add] writing `./config/schedule.rb'
[done] wheneverized!

All that the command does is create a schedule.rb file in the config directory where we can write our cron jobs. The generated file has some commented-out examples in it that are worth reading through, but we’re going to delete them and start again.

Writing Jobs

Let’s say that we’re using the excellent Thinking Sphinx plugin to manage the searching in our application. We’ll need to update the index on a regular basis, say every two hours, by running a rake task. We can do this by writing the following code in schedule.rb.

every 2.hours do
  rake "thinking_sphinx:index"
end

Alternatively, instead of specifying a time period we can use certain keywords. For example we could start Thinking Sphinx when the server boots up.

every :reboot do
  rake "thinking_sphinx:start"
end

Let’s say that the size of our cache files is getting out of hand so we want to clear out the cache folder every Friday morning at 4am. We can do this by running rm in the cache folder.

every :friday, :at => "4am" do
  command "rm -rf #{RAILS_ROOT}/tmp/cache"
end

Each block can take multiple commands, so if we wanted to also run some Ruby code at 4am every Friday we can. If our application was a online store that used carts and we had a class method on our Cart model to remove abandoned carts we could run it at the same time we clear our cache.

every :friday, :at => "4am" do
  command "rm -rf #{RAILS_ROOT}/tmp/cache"
  runner "Cart.remove_abandoned"
end

Generating The Cron Jobs

Now that we’ve defined our jobs in Ruby code how to we turn them into cron jobs? From our application’s directory we can call whenever --update-crontab and pass an identifier that uniquely identifies our application, “store” in our case.

whenever --update-crontab store

The identifier is important as it ensures that the correct cron jobs are updated when we change our schedule.rb file. After we run the command we can open the crontab file with crontab -l and see the jobs that Whenever has added.

# Begin Whenever generated tasks for: store
0 0,2,4,6,8,10,12,14,16,18,20,22 * * * cd /Users/eifion/rails/apps_for_asciicasts/ep164 && RAILS_ENV=production /usr/bin/env rake thinking_sphinx:index
0 4 * * fri rm -rf /Users/eifion/rails/apps_for_asciicasts/ep164/tmp/cache
0 4 * * fri /Users/eifion/rails/apps_for_asciicasts/ep164/script/runner -e production "Cart.remove_abandoned"
@reboot cd /Users/eifion/rails/apps_for_asciicasts/ep164 && RAILS_ENV=production /usr/bin/env rake thinking_sphinx:start
# End Whenever generated tasks for: store

Our crontab file now has jobs to run the Thinking Sphinx index rake task every two hours; to clear the cache and remove the abandoned carts at 4am on Friday and to start Thinking Sphinx on reboot.

Making Changes

Whenever makes it easy to make changes to your jobs too. If we want the tasks that run on Friday at 4am to run at Saturday at, say, 4.38am we can just make the appropriate changes and rerun whenever.

every :saturday, :at => "4:38am" do
  command "rm -rf #{RAILS_ROOT}/tmp/cache"
  runner "Cart.remove_abandoned"
end

If we rerun whenever now and view the crontab file we’ll see the updated jobs.

$ whenever --update-crontab store
[write] crontab file updated
$ crontab -l
# Begin Whenever generated tasks for: store
38 4 * * sat rm -rf /Users/eifion/rails/apps_for_asciicasts/ep164/tmp/cache
38 4 * * sat /Users/eifion/rails/apps_for_asciicasts/ep164/script/runner -e production "Cart.remove_abandoned"
0 0,2,4,6,8,10,12,14,16,18,20,22 * * * cd /Users/eifion/rails/apps_for_asciicasts/ep164 && RAILS_ENV=production /usr/bin/env rake thinking_sphinx:index
@reboot cd /Users/eifion/rails/apps_for_asciicasts/ep164 && RAILS_ENV=production /usr/bin/env rake thinking_sphinx:start
# End Whenever generated tasks for: store

Updating Automatically With Capistrano

You’ll probably want to update our cron jobs each time you deploy your application. If you’re using Capistrano then you can update its deploy.rb file so that the crontab file is updated automatically. The code you need to add is:

after "deploy:symlink", "deploy:update_crontab"
namespace :deploy do
  desc "Update the crontab file"
  task :update_crontab, :roles => :db do
    run "cd #{release_path} && whenever --update-crontab #{application}"
  end
end

After Capistrano symlinks the release directory file to the current one it will run the whenever command, passing in the name of the application as the identifier and your crontab file will be updated automatically.