Our Blog
Tags
Recurring tasks in Ruby on Rails using runner and cron jobs
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 run ruby 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.
Find related posts: cron jobs tutorial ruby on rails
Comments
When I am editing the crontab, is there a certain place to save the file. The default on mine is in the /tmp/ folder which doesn't seem right. Any help would be appreciated.
PJohn: That is normal behavior for the crontab editor. I believe it is done this way to perform the syntax checking on the crontab file you are about to save. If you enter some bad syntax in the file, an error will be returned. If it is properly formatted, saving the file and exiting your editor (Vim, emacs, or whatever your default shell editor is) will result in a successful message that the crontab file was installed/updated.
Thxs Kip I was able to get it to edit. My problem resulted in misinterpreting the # symbol that stood for a comment line as the file wanting an id number for the task. Doh!
I get new mails…
maybe you'd have an idea about it, the message's the following :
<pre>Missing the Rails 2.1.0 gem. Please `gem install -v=2.1.0 rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.</pre>
Thanks for this article, maybe the clearest explanation that could be found.
Hi, i set the cronjob as you told. the script/runner command is working fine in command prompt.. but i am unable to run the cronjob.
My application is hosted in Hostgator!
when i run the "crontab -e" command in telnet its created a crontab file in "tmp" directory. in that i placed the command.
but it is not running.
i need your help to resolve this issue.
nagesh: When you run "crontab -e" in your shell, it should open the crontab file in your default editor (probably vi). After you save and exit the editor, you should receive the message "crontab file installed successfully." Are you getting that message? If you are not, there are likely syntax errors in the file.
Often I have a syntax error due to line wrapping within the editor. You need to make sure that the command you are putting in there is only on a single line. You may have to max your terminal window's width to be able to fix the wrapping.
Thank you Kip, this is very helpful!
Thank-you. Neat & simple.
Only problem was the command got entered 5 times! I've no idea how that happened, either. Must be vi command I didn't know I'd done. (Ha!)
The output was several of the same messages being sent, when I was expecting only one. Looking at the interleaving in the log file finally clued me in.
Thank=you again, Dirk
There is a slight syntax error in the cron example, at least on CentOS 5, it requires an extra '/' before the ruby executable in place of the space.
30 14 * * * /usr/bin/env/ruby /Users/kip/svn/thewall/script/runner /Users/kip/svn/thewall/app/delete_old_posts.rb