Recurring tasks in Ruby on Rails using runner and cron jobs
Update (4/25/2009): I recommend using rake tasks as opposed to script/runner for recurring tasks. You can read all about how to use rake at this thorough RailsEnvy blog post.
As you start building more and more complex web applications, you will surely run into a need to perform some kind of recurring jobs/tasks to keep your website members or database data in check. Luckily, Rails comes with a script called "runner" that makes this very easy to handle. There are many ways to run recurring jobs with Rails, but I believe this is the simplest one, and will be easy for even amateur Rails programmers to understand.
script/console and script/runner
You've used script/console many times, and probably realize how crucial it is to keeping you happy when developing a new application. When you run the console, you can pick which application environment to run it in (development by default), and you can run commands just like you would in a controller of your application, instantly interacting with the application. Yeah, it's just plain awesome.
Great news — script/runner works exactly the same way, only instead of passing a line at a time, you can feed it an entire Ruby script. It interacts with your environment the same way that the console does. We can take that command, and using a cron job, we can have it run at a recurring schedule!
cron jobs
A cron job is simply a scheduling service for UNIX. Why the heck is it called cron? It's taken from the Greek word chronos, meaning time. Yeah, I Wikipedia'ed that at some point, so what? We'll get more into the details of setting up your Ruby script with its cron job in a little while.
"The Wall"
Let's make a very simple application. The concept is that the homepage will show a list of messages, and then display a form underneath that allows the visitor to post their own message. It will consist of a single model called Post. I'm not going to show you how to build out this boring application, but instead just create the model and populate it with some fake data, in order to show you how to use script/runner to interact with the application.
In a real world situation, you'd be doing some nightly task like checking to see if any website member has an upcoming expiration date on their credit card. You'd then want to send them an email alert telling them to update their credit card data before their membership is deactivated (by another script that gets run every night!).
However, for the sake of our application and its simplicity, our task will simply be to remove posts that are over five minutes old. We will be deleting them automatically from the database, to keep it tiny and only show the newest posts. Yes, this seems pointless, but too bad! It's my way of teaching, darn it.
Create the sample application
Yeah, I'm really going to show this :-)
rails thewall cd thewall script/generate model post
001_create_posts.rb
class CreatePosts < ActiveRecord::Migration def self.up create_table :posts do |t| t.text :message, :null => false t.timestamps end end def self.down drop_table :posts end end
post.rb
class Post < ActiveRecord::Base validates_presence_of :message end
Don't forget to run the migration...
rake db:migrate
Add some sample messages
So we can test our nightly script immediately, I'm going to manually set the created time for some of these sample postings.
script/console Loading development environment (Rails 2.0.2) >> Post.create(:message => 'Lorem ipsum dolor sit amet.', :created_at => 10.minutes.ago) >> Post.create(:message => 'Consectetuer adipiscing elit.', :created_at => 8.minutes.ago) >> Post.create(:message => 'Risus id euismod dictum.', :created_at => 6.minutes.ago) >> Post.create(:message => 'Duis porttitor posuere nulla.', :created_at => 4.minutes.ago) >> Post.create(:message => 'Maecenas odio dolor.', :created_at => 2.minutes.ago) >> Post.create(:message => 'Praesent at turpis.')
Creating the script for nightly use
I'm going to create the following script in my /app folder. As you make more of these kinds of scripts for the same application, it's a good idea to organize them better than I am here. So, all we need our script to do is delete all Post records from the database that were created over 5 minutes ago.
delete_old_posts.rb
class DeleteOldPosts < ActiveRecord::Base # This script deletes all posts that are over 5 minutes old post_ids = Post.find(:all, :conditions => ["created_at < ?", 5.minutes.ago]) if post_ids.size > 0 Post.destroy(post_ids) puts "#{post_ids.size} posts have been deleted!" end end
This is pretty straight forward. It finds all posts whose created_at datetime field is before the time it was 5 minutes ago. If it finds any, it deletes them and then prints a message saying how many posts were deleted. Let's run it to see how it does.
script/runner app/delete_old_posts.rb
3 posts have been deleted
Sweet! Looks like it did the job. Now all we need to do is setup a cron job so this script is run every night.
Setting up the cron job
First, we will figure out the entire command we want to run. We're going to need to runruby on the script/runner script, and pass it the name of the Ruby script we want it to run. Here's our command:
/usr/bin/env ruby /Users/kip/svn/thewall/script/runner /Users/kip/svn/thewall/app/delete_old_posts.rb
Be sure to change your paths appropriately for where your application is located on your disk.
Next, we need to schedule this command to be run at a recurring interval. This is where the cron job comes in. To edit your cron jobs, we will be editing the crontab file, which holds all the cron job schedules. To edit your crontab file, use the crontab -e command. Your default shell text editor will be used to edit the file.
crontab -e
We now see our crontab file, which is most likely blank at this point. We need to add a line that tells it a schedule and a command to run. The syntax of each crontab line is six fields separated by spaces: minute hour day-of-month month day-of-week command. The first five fields can take a asterisk (*), which means all, a single number, a list of numbers separated by commas, or a range which is separated by a dash.
Important: Each crontab job you setup must be on a single line. If the line wraps at any point it will give you a syntax error when you try to save the crontab. So, if you need to maximize your terminal window or use a different editor to get the wrapping under control, do so.
Here are some schedule examples:
# Run mycommand at the top of every hour, from 0:00 to 23:00 0 0-23 * * * mycommand # Run mycommand every 30 minutes 0,30 * * * * mycommand # Run mycommand at midnight every weekday (sunday is 0, saturday is 6) 0 0 * * 1-5 mycommand
Since I want to see this script work for myself soon, I'm going to set it to run 5 minutes from now. This way, I can check my database in a little while and see if it worked!
30 14 * * * /usr/bin/env ruby /Users/kip/svn/thewall/script/runner /Users/kip/svn/thewall/app/delete_old_posts.rb
So, I set it to run at 2:30 PM, which is coming up soon. I just wait until then, open up my Rails console, and run a quick query to see how many Post records I have. If it changes, it worked :-)
For further information on crontabs and how to use them, take a look at this article. Especially useful is the section that talks about suppressing the local mail that is delivered whenever the cron job runs.

