rufus-scheduler
rufus-scheduler is a Ruby gem for scheduling pieces of code (jobs). It understands running a job AT a certain time, IN a certain time, EVERY x time or simply via a CRON statement.
rufus-scheduler is no replacement for cron/at since it runs inside of Ruby.
alternatives / complements
A list of related Ruby projects :
More like complements :
installation
gem install rufus-scheduler
usage
The usage is similar to the one of the old rufus-scheduler. There are a few differences though.
require 'rubygems'
require 'rufus/scheduler'
scheduler = Rufus::Scheduler.start_new
scheduler.in '20m' do
puts "order ristretto"
end
scheduler.at 'Thu Mar 26 07:31:43 +0900 2009' do
puts 'order pizza'
end
scheduler.cron '0 22 * * 1-5' do
# every day of the week at 22:00 (10pm)
puts 'activate security system'
end
scheduler.every '5m' do
puts 'check blood pressure'
end
This code summons a plain version of the scheduler, this can be made more explicit via :
scheduler = Rufus::Scheduler::PlainScheduler.start_new
This PlainScheduler accepts a :thread_name option :
scheduler = Rufus::Scheduler::PlainScheduler.start_new(:thread_name => 'my scheduler')
might be helpful when tracking threads.
Note that is there is EventMachine present and running,
scheduler = Rufus::Scheduler.start_new
will return an instance of Rufus::Scheduler::EmScheduler (leveraging EventMachine).
a note about cron jobs
This is a classical cron :
scheduler.cron '0 22 * * 1-5' do
# every day of the week at 22:00 (10pm)
end
Rufus-scheduler supports two variants to that notation : seconds and timezones.
scheduler.cron '13 0 22 * * 1-5' do
# every day of the week at 22:00:13
end
scheduler.cron '0 22 * * 1-5 Europe/Paris' do
# every day of the week when it's 22:00 in Paris
end
scheduler.cron '0 22 * * 1-5 Etc/GMT+2' do
# every day of the week when it's 22:00 in GMT+2
end
The timezones are the ones supported by the ‘tzinfo’ rubygem (tzinfo.rubyforge.org/).
The timezone support was contributed by Tanzeeb Khalili.
Since 2.0.9, “monthdays” are supported
scheduler.cron '0 22 * * sun#1,sun#2' do
# every first and second sunday of the month, at 22:00
end
scheduler.join
Note that if you have a tiny script like this one :
require 'rubygems'; require 'rufus-scheduler'
scheduler = Rufus::Scheduler.start_new
scheduler.at 'Thu Mar 26 07:31:43 +0900 2009' do
puts 'order pizza'
end
And you run it, it will exit immediately.
If you place
scheduler.join
at then end of it will make the current (main) thread join the scheduler and prevent the Ruby runtime from exiting.
You shouldn’t be exposed to this issue when using EventMachine, since while running EM, your runtime won’t exit.
important note about #join
DO NOT CALL this #join method if you’re running rufus-scheduler from Rails or Sinatra or any application that’s already some kind of ‘daemon’. It’s not necessary! #join is meant for small standalone scripts.
block parameters
Scheduled blocks accept 0 or 1 parameter (this unique parameter is the job instance itself).
scheduler.every '5m' do
puts 'check blood pressure'
end
scheduler.every '1y' do |job|
puts "check cholesterol levels (#{job.job_id})"
end
See the class Job for more details :
rufus.rubyforge.org/rufus-scheduler/classes/Rufus/Scheduler/Job.html
the time strings understood by rufus-scheduler
require 'rubygems'
require 'rufus/scheduler'
p Rufus.parse_time_string '500' # => 0.5
p Rufus.parse_time_string '1000' # => 1.0
p Rufus.parse_time_string '1h' # => 3600.0
p Rufus.parse_time_string '1h10s' # => 3610.0
p Rufus.parse_time_string '1w2d' # => 777600.0
p Rufus.to_time_string 60 # => "1m"
p Rufus.to_time_string 3661 # => "1h1m1s"
p Rufus.to_time_string 7 * 24 * 3600 # => "1w"
:blocking => true
Jobs will, by default, trigger in their own thread. This is usually desirable since one expects the scheduler to continue scheduling even if a job is currently running.
Jobs scheduled with the :blocking parameter will run in the thread of the scheduler, blocking it.
scheduler.in '20m', :blocking => true do
puts "order ristretto"
sleep 2 * 60
end
scheduler.in '21m' do
puts "order espresso"
end
Hence, our espresso will come in 22 minutes instead of 21.
Warning, ‘cron’ behaves a bit differently than ‘in’ and ‘at’, if the scheduler is blocked working on a task, it may skip crons (while ins and ats get scheduled after).
scheduler.cron '0 16 * * * *' do
puts "four o'clock tea"
end
If at 4pm the scheduler is in a blocking task, there will be no four o’clock tea.
:mutex => ‘that_mutex’
:blocking is nice but it is blocking the whole scheduler. What about something more fine-grained ? And also something that can be used with in, at, every and cron ?
scheduler.in '20m', :mutex => 'that_mutex' do
puts "order ristretto"
sleep 2 * 60
puts "ah, that was delicious"
end
scheduler.in '21m' :mutex => 'that_mutex' do
puts "order espresso"
end
the “order espresso” will only get triggered once the ristretto has been consumed. Rufus-scheduler, will create a ‘that_mutex’ mutex and keep track of it. Don’t go on passing too many different mutex names, rufus-scheduler will keep track of each of them (they won’t get garbage collected).
It’s OK to use a mutex directly:
m = Mutex.new
# ...
scheduler.cron '0 18 * * *', :mutex => m do
# ...
end
scheduler.in '21m' :mutex => m do
# ...
end
It can be handy for even more fine-grained control:
m = Mutex.new
# ...
scheduler.cron '0 18 * * *', :mutex => m do
# ...
end
scheduler.in '21m' do
# non-critical
m.synchronize do
# critical
end
# non-critical
end
:allow_overlapping => false
By default, every and cron jobs will “overlap”:
scheduler.every '3s' do
4.times do |i|
puts "hello #{i}"
sleep 1
end
end
You mind end up with something that looks like
hello 0
hello 1
hello 2
hello 3
hello 3
hello 4
...
This every job, will have overlaps. To prevent that:
scheduler.every '3s', :allow_overlapping => false do
# ...
end
‘every’ jobs and :first_at / :first_in
This job will execute every 3 days, but first time will be in 5 days from now :
scheduler.every '3d', :first_in => '5d' do
# do something
end
This job will execute every 3 days, starting from Christmas Eve at noon :
scheduler.every '3d', :first_at => '2009/12/24 12:00' do
# do something
end
The chronic gem may help (chronic.rubyforge.org/) :
require 'chronic' # sudo gem install chronic
scheduler.every '3h', :first_at => Chronic.parse('this tuesday 5:00') do
# do something starting this tueday
end
Note : setting a :first_at/:first_in in the past will get rufus-scheduler to trigger for all the past schedules until now. Adding :discard_past => true will prevent this.
self unschedule for ‘cron’ and ‘every’ jobs
‘at’ and ‘in’ jobs fire once only. ‘cron’ and ‘every’ jobs do fire repeatedly, so it might be useful to stop them.
scheduler.every '3d' do |job|
l = determine_crop_maturity_level()
if l >= 7
puts "crop is ready."
job.unschedule
else
puts "crop not yet ready..."
end
end
In this example, the ‘every’ job will unschedule itself when the crop is ready.
#running?
job = scheduler.every '3d' do
# ...
end
# ...
p job.running?
Job#running? will return true when the job got triggered and is actually performing.
Please note, that #running? is not related to the #paused? which is detailed in the next section.
#pause, #resume and #paused?
Jobs, as well as the scheduler itself have a pair of #pause and #resume methods.
job = scheduler.every '2h' do
# ...
end
# ...
job.pause # the job will be scheduled but won't trigger
# ...
puts job.paused?
Pausing / resuming a job doesn’t affect the scheduling of a job, it merely “silences” it, its block won’t get executed. Calling resume will not reset the schedule of the job. If you schedule a job to trigger every 10 minutes at 10am and pause it from 1020 to 1025, it’s next triggering time will be 1030 approximately.
One can pause an “at” or “in” job. If it’s still paused at trigger time, it will simply become a dud.
As said, the scheduler has a #pause(job_or_job_id) and a #resume(job_or_job_id) pair of methods:
scheduler.pause(job)
scheduler.pause(job_id)
scheduler.resume(job)
scheduler.resume(job_id)
Scheduler#running_jobs
One can get a list of the jobs just triggered (actually running) by doing
jobs = scheduler.running_jobs
schedulables
Sometimes passing a block isn’t that convenient :
class JobThing
def initialize(relevant_info)
@ri = relevant_info
end
def call(job)
do_something_about_it
end
end
# ...
scheduler.in '3d', JobThing.new('http://news.example.com/data_xyz')
scheduler.in '1w', JobThing.new('http://news.example.com/data_abc'), :timeout => '1d'
rufus-scheduler accepts anything that responds to a call method with a unique parameter (it will pass the job) as a ‘schedulable’.
For compatibility with older (1.x) versions, schedulables with a trigger methods are accepted :
class JobThing
def trigger(params)
job = params[:job]
end
end
The ‘params’ correspond to the scheduler job params, and the key :job points to the rufus-scheduler job for the schedulable that is passed to a ‘call schedulable’.
looking up jobs
scheduler.jobs
# returns a map job_id => job of at/in/every jobs
scheduler.cron_jobs
# idem for cron jobs
scheduler.all_jobs
# idem but for every/at/in/cron jobs (all of them)
scheduler.find_by_tag(t)
# returns all the jobs with a given tag (passed at schedule time with :tags)
unscheduling jobs
The ‘scheduling’ methods always return an instance of Rufus::Scheduler::Job. This object can be used for unscheduling :
job = scheduler.in '2d', :tags => 'admin' do
run_backlog_cleaning()
end
# later ...
job.unschedule
# or
scheduler.unschedule(job.job_id)
tags
You can specify tags at schedule time :
scheduler.in '2d', :tags => 'admin' do
run_backlog_cleaning()
end
scheduler.every '3m', :tags => 'production' do
check_order_log()
end
And later query the scheduler for those jobs :
admin_jobs = scheduler.find_by_tag('admin')
production_jobs = scheduler.find_by_tag('production')
timeout
One can specify a timeout for the triggering of a job.
scheduler.every '2d', :timeout => '40m' do
begin
run_backlog_cleaning()
rescue Rufus::Scheduler::TimeOutError => toe
# timeout occurred
end
end
This job will run every two days. If a run takes more than 40 minutes it will timeout (its thread will receive a TimeOutError).
This timeout feature relies on an ‘in’ job scheduled at the moment the main job gets triggered, hence the ‘40m’ time string format.
exceptions in jobs
By default, when exception occur when a job performs, the error message will be output to the STDOUT.
It’s easy to customize that behaviour :
scheduler = Rufus::Scheduler::PlainScheduler.start_new
# or
#scheduler = Rufus::Scheduler::EmScheduler.start_new
def scheduler.handle_exception(job, exception)
puts "job #{job.job_id} caught exception '#{exception}'"
end
These are OK too:
def scheduler.on_exception(job, exception)
puts "job #{job.job_id} caught exception '#{exception}'"
end
# or
def scheduler.on_exception(exception)
puts "caught exception '#{exception}'"
For backward compatibility, overriding #log_exception is still OK :
def scheduler.log_exception(exception)
puts "caught exception '#{exception}'"
end
Note that an every job or a cron job will stay scheduled even if it experiences an exception.
frequency
The default frequency for the scheduler is 0.330 seconds. This means that the usual scheduler implementation will wake up, trigger jobs that are to be triggered and then go back to sleep for 0.330 seconds. Note that this doesn’t mean that the scheduler will wake up very 0.330 seconds (checking and triggering do take time).
You can set a different frequency when starting / initializing the scheduler :
require 'rubygems'
require 'rufus/scheduler'
scheduler = Rufus::Scheduler.start_new(:frequency => 60.0)
# for a lazy scheduler that only wakes up every 60 seconds
usage with EventMachine
rufus-scheduler 2.0 can be used in conjunction with EventMachine (github.com/eventmachine/eventmachine/).
More and more ruby applications are using EventMachine. This flavour of the scheduler relies on EventMachine, thus it doesn’t require a separate thread like the PlainScheduler does.
require 'rubygems'
require 'eventmachine'
EM.run {
scheduler = Rufus::Scheduler::EmScheduler.start_new
scheduler.in '20m' do
puts "order ristretto"
end
}
with Passenger
“it terminates for no apparent reason !”
github.com/jmettraux/rufus-scheduler/issues/issue/10
tested with
-
1.8.7-p249
-
1.9.2-p290
-
jruby-1.5.1
on Mac OS X (Snow Leopard).
dependencies
The ‘tzinfo’ rubygem.
The ruby gem ‘eventmachine’ if you use Rufus::Scheduler::EmScheduler, else no other dependencies.
mailing list
On the rufus-ruby list :
groups.google.com/group/rufus-ruby
issue tracker
rubyforge.org/tracker/?atid=18584&group_id=4812&func=browse
irc
irc.freenode.net #ruote
source
github.com/jmettraux/rufus-scheduler
git clone git://github.com/jmettraux/rufus-scheduler.git
credits
github.com/jmettraux/rufus-scheduler/blob/master/CREDITS.txt
authors
John Mettraux, [email protected], jmettraux.wordpress.com
the rest of Rufus
license
MIT