├── .gitignore ├── README.md ├── Rakefile ├── VERSION ├── bin └── clockwork ├── clockwork.gemspec ├── example.rb ├── lib └── clockwork.rb └── test └── clockwork_test.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This fork is no longer maintained, maybe try this one: [https://github.com/Rykian/clockwork](https://github.com/Rykian/clockwork)** 2 | 3 | --- 4 | 5 | Clockwork - a clock process to replace cron 6 | =========================================== 7 | 8 | Cron is non-ideal for running scheduled application tasks, especially in an app 9 | deployed to multiple machines. [More details.](http://adam.heroku.com/past/2010/4/13/rethinking_cron/) 10 | 11 | Clockwork is a cron replacement. It runs as a lightweight, long-running Ruby 12 | process which sits alongside your web processes (Mongrel/Thin) and your worker 13 | processes (DJ/Resque/Minion/Stalker) to schedule recurring work at particular 14 | times or dates. For example, refreshing feeds on an hourly basis, or send 15 | reminder emails on a nightly basis, or generating invoices once a month on the 16 | 1st. 17 | 18 | Quickstart 19 | ---------- 20 | 21 | Create clock.rb: 22 | 23 | require 'clockwork' 24 | include Clockwork 25 | 26 | handler do |job| 27 | puts "Running #{job}" 28 | end 29 | 30 | every(10.seconds, 'frequent.job') 31 | every(3.minutes, 'less.frequent.job') 32 | every(1.hour, 'hourly.job') 33 | 34 | every(1.day, 'midnight.job', :at => '00:00') 35 | 36 | Run it with the clockwork binary: 37 | 38 | $ clockwork clock.rb 39 | Starting clock for 4 events: [ frequent.job less.frequent.job hourly.job midnight.job ] 40 | Triggering frequent.job 41 | 42 | Use with queueing 43 | ----------------- 44 | 45 | The clock process only makes sense as a place to schedule work to be done, not 46 | to do the work. It avoids locking by running as a single process, but this 47 | makes it impossible to parallelize. For doing the work, you should be using a 48 | job queueing system, such as 49 | [Delayed Job](http://www.therailsway.com/2009/7/22/do-it-later-with-delayed-job), 50 | [Beanstalk/Stalker](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/), 51 | [RabbitMQ/Minion](http://adamblog.heroku.com/past/2009/9/28/background_jobs_with_rabbitmq_and_minion/), or 52 | [Resque](http://github.com/blog/542-introducing-resque). This design allows a 53 | simple clock process with no locks, but also offers near infinite horizontal 54 | scalability. 55 | 56 | For example, if you're using Beanstalk/Staker: 57 | 58 | require 'stalker' 59 | 60 | handler { |job| Stalker.enqueue(job) } 61 | 62 | every(1.hour, 'feeds.refresh') 63 | every(1.day, 'reminders.send', :at => '01:30') 64 | 65 | Using a queueing system which doesn't require that your full application be 66 | loaded is preferable, because the clock process can keep a tiny memory 67 | footprint. If you're using DJ or Resque, however, you can go ahead and load 68 | your full application enviroment, and use per-event blocks to call DJ or Resque 69 | enqueue methods. For example, with DJ/Rails: 70 | 71 | require 'config/boot' 72 | require 'config/environment' 73 | 74 | every(1.hour, 'feeds.refresh') { Feed.send_later(:refresh) } 75 | every(1.day, 'reminders.send', :at => '01:30') { Reminder.send_later(:send_reminders) } 76 | 77 | Anatomy of a clock file 78 | ----------------------- 79 | 80 | clock.rb is standard Ruby. Since we include the Clockwork module (the 81 | clockwork binary does this automatically, or you can do it explicitly), this 82 | exposes a small DSL ("handler" and "every") to define the handler for events, 83 | and then the events themselves. 84 | 85 | The handler typically looks like this: 86 | 87 | handler { |job| enqueue_your_job(job) } 88 | 89 | This block will be invoked every time an event is triggered, with the job name 90 | passed in. In most cases, you should be able to pass the job name directly 91 | through to your queueing system. 92 | 93 | The second part of the file are the events, which roughly resembles a crontab: 94 | 95 | every(5.minutes, 'thing.do') 96 | every(1.hour, 'otherthing.do') 97 | 98 | In the first line of this example, an event will be triggered once every five 99 | minutes, passing the job name 'thing.do' into the handler. The handler shown 100 | above would thus call enqueue_your_job('thing.do'). 101 | 102 | You can also pass a custom block to the handler, for job queueing systems that 103 | rely on classes rather than job names (i.e. DJ and Resque). In this case, you 104 | need not define a general event handler, and instead provide one with each 105 | event: 106 | 107 | every(5.minutes, 'thing.do') { Thing.send_later(:do) } 108 | 109 | If you provide a custom handler for the block, the job name is used only for 110 | logging. 111 | 112 | You can also use blocks to do more complex checks: 113 | 114 | every(1.day, 'check.leap.year') do 115 | Stalker.enqueue('leap.year.party') if Time.now.year % 4 == 0 116 | end 117 | 118 | In production 119 | ------------- 120 | 121 | Only one clock process should ever be running across your whole application 122 | deployment. For example, if your app is running on three VPS machines (two app 123 | servers and one database), your app machines might have the following process 124 | topography: 125 | 126 | * App server 1: 3 web (thin start), 3 workers (rake jobs:work), 1 clock (clockwork clock.rb) 127 | * App server 2: 3 web (thin start), 3 workers (rake jobs:work) 128 | 129 | You should use Monit, God, Upstart, or Inittab to keep your clock process 130 | running the same way you keep your web and workers running. 131 | 132 | Meta 133 | ---- 134 | 135 | Created by Adam Wiggins 136 | 137 | Inspired by [rufus-scheduler](http://rufus.rubyforge.org/rufus-scheduler/) and [http://github.com/bvandenbos/resque-scheduler](resque-scehduler) 138 | 139 | Design assistance from Peter van Hardenberg and Matthew Soldo 140 | 141 | Patches contributed by Mark McGranaghan and Lukáš Konarovský 142 | 143 | Released under the MIT License: http://www.opensource.org/licenses/mit-license.php 144 | 145 | http://github.com/adamwiggins/clockwork 146 | 147 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'jeweler' 2 | 3 | Jeweler::Tasks.new do |s| 4 | s.name = "clockwork" 5 | s.summary = "A scheduler process to replace cron." 6 | s.description = "A scheduler process to replace cron, using a more flexible Ruby syntax running as a single long-running process. Inspired by rufus-scheduler and resque-scheduler." 7 | s.author = "Adam Wiggins" 8 | s.email = "adam@heroku.com" 9 | s.homepage = "http://github.com/adamwiggins/clockwork" 10 | s.executables = [ "clockwork" ] 11 | s.rubyforge_project = "clockwork" 12 | 13 | s.files = FileList["[A-Z]*", "{bin,lib}/**/*"] 14 | end 15 | 16 | Jeweler::GemcutterTasks.new 17 | 18 | task 'test' do 19 | sh "ruby test/clockwork_test.rb" 20 | end 21 | 22 | task :build => :test 23 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.4 2 | -------------------------------------------------------------------------------- /bin/clockwork: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | STDERR.sync = STDOUT.sync = true 4 | 5 | require File.expand_path('../../lib/clockwork', __FILE__) 6 | include Clockwork 7 | 8 | usage = "clockwork " 9 | file = ARGV.shift or abort usage 10 | 11 | file = "./#{file}" unless file.match(/^[\/.]/) 12 | 13 | require file 14 | 15 | trap('INT') do 16 | puts "\rExiting" 17 | exit 18 | end 19 | 20 | run 21 | -------------------------------------------------------------------------------- /clockwork.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{clockwork} 8 | s.version = "0.2.4" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Adam Wiggins"] 12 | s.date = %q{2011-07-31} 13 | s.default_executable = %q{clockwork} 14 | s.description = %q{A scheduler process to replace cron, using a more flexible Ruby syntax running as a single long-running process. Inspired by rufus-scheduler and resque-scheduler.} 15 | s.email = %q{adam@heroku.com} 16 | s.executables = ["clockwork"] 17 | s.extra_rdoc_files = [ 18 | "README.md" 19 | ] 20 | s.files = [ 21 | "README.md", 22 | "Rakefile", 23 | "VERSION", 24 | "bin/clockwork", 25 | "lib/clockwork.rb" 26 | ] 27 | s.homepage = %q{http://github.com/adamwiggins/clockwork} 28 | s.require_paths = ["lib"] 29 | s.rubyforge_project = %q{clockwork} 30 | s.rubygems_version = %q{1.5.0} 31 | s.summary = %q{A scheduler process to replace cron.} 32 | s.test_files = [ 33 | "test/clockwork_test.rb" 34 | ] 35 | 36 | if s.respond_to? :specification_version then 37 | s.specification_version = 3 38 | 39 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 40 | else 41 | end 42 | else 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /example.rb: -------------------------------------------------------------------------------- 1 | require 'clockwork' 2 | include Clockwork 3 | 4 | handler do |job| 5 | puts "Queueing job: #{job}" 6 | end 7 | 8 | every(10.seconds, 'run.me.every.10.seconds') 9 | every(1.minute, 'run.me.every.minute') 10 | every(1.hour, 'run.me.every.hour') 11 | 12 | every(1.day, 'run.me.at.midnight', :at => '00:00') 13 | 14 | every(1.day, 'custom.event.handler', :at => '00:30') do 15 | puts "This event has its own handler" 16 | end 17 | -------------------------------------------------------------------------------- /lib/clockwork.rb: -------------------------------------------------------------------------------- 1 | module Clockwork 2 | class FailedToParse < StandardError; end; 3 | 4 | class Event 5 | attr_accessor :job, :last 6 | 7 | def initialize(period, job, block, options={}) 8 | @period = period 9 | @job = job 10 | @at = parse_at(options[:at]) 11 | @last = nil 12 | @block = block 13 | end 14 | 15 | def to_s 16 | @job 17 | end 18 | 19 | def time?(t) 20 | ellapsed_ready = (@last.nil? or (t - @last).to_i >= @period) 21 | time_ready = (@at.nil? or (t.hour == @at[0] and t.min == @at[1])) 22 | ellapsed_ready and time_ready 23 | end 24 | 25 | def run(t) 26 | @last = t 27 | @block.call(@job) 28 | rescue => e 29 | log_error(e) 30 | end 31 | 32 | def log_error(e) 33 | STDERR.puts exception_message(e) 34 | end 35 | 36 | def exception_message(e) 37 | msg = [ "Exception #{e.class} -> #{e.message}" ] 38 | 39 | base = File.expand_path(Dir.pwd) + '/' 40 | e.backtrace.each do |t| 41 | msg << " #{File.expand_path(t).gsub(/#{base}/, '')}" 42 | end 43 | 44 | msg.join("\n") 45 | end 46 | 47 | def parse_at(at) 48 | return unless at 49 | m = at.match(/^(\d\d):(\d\d)$/) 50 | raise FailedToParse, at unless m 51 | hour, min = m[1].to_i, m[2].to_i 52 | raise FailedToParse, at if hour >= 24 or min >= 60 53 | [ hour, min ] 54 | end 55 | end 56 | 57 | extend self 58 | 59 | def handler(&block) 60 | @@handler = block 61 | end 62 | 63 | class NoHandlerDefined < RuntimeError; end 64 | 65 | def get_handler 66 | raise NoHandlerDefined unless (defined?(@@handler) and @@handler) 67 | @@handler 68 | end 69 | 70 | def every(period, job, options={}, &block) 71 | event = Event.new(period, job, block || get_handler, options) 72 | @@events ||= [] 73 | @@events << event 74 | event 75 | end 76 | 77 | def run 78 | log "Starting clock for #{@@events.size} events: [ " + @@events.map { |e| e.to_s }.join(' ') + " ]" 79 | loop do 80 | tick 81 | sleep 1 82 | end 83 | end 84 | 85 | def log(msg) 86 | puts msg 87 | end 88 | 89 | def tick(t=Time.now) 90 | to_run = @@events.select do |event| 91 | event.time?(t) 92 | end 93 | 94 | to_run.each do |event| 95 | log "Triggering #{event}" 96 | event.run(t) 97 | end 98 | 99 | to_run 100 | end 101 | 102 | def clear! 103 | @@events = [] 104 | @@handler = nil 105 | end 106 | 107 | end 108 | 109 | unless 1.respond_to?(:seconds) 110 | class Numeric 111 | def seconds; self; end 112 | alias :second :seconds 113 | 114 | def minutes; self * 60; end 115 | alias :minute :minutes 116 | 117 | def hours; self * 3600; end 118 | alias :hour :hours 119 | 120 | def days; self * 86400; end 121 | alias :day :days 122 | end 123 | end -------------------------------------------------------------------------------- /test/clockwork_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../lib/clockwork', __FILE__) 2 | require 'contest' 3 | require 'mocha' 4 | require 'time' 5 | 6 | module Clockwork 7 | def log(msg) 8 | end 9 | end 10 | 11 | class ClockworkTest < Test::Unit::TestCase 12 | setup do 13 | Clockwork.clear! 14 | Clockwork.handler { } 15 | end 16 | 17 | def assert_will_run(t) 18 | assert_equal 1, Clockwork.tick(t).size 19 | end 20 | 21 | def assert_wont_run(t) 22 | assert_equal 0, Clockwork.tick(t).size 23 | end 24 | 25 | test "once a minute" do 26 | Clockwork.every(1.minute, 'myjob') 27 | 28 | assert_will_run(t=Time.now) 29 | assert_wont_run(t+30) 30 | assert_will_run(t+60) 31 | end 32 | 33 | test "every three minutes" do 34 | Clockwork.every(3.minutes, 'myjob') 35 | 36 | assert_will_run(t=Time.now) 37 | assert_wont_run(t+2*60) 38 | assert_will_run(t+3*60) 39 | end 40 | 41 | test "once an hour" do 42 | Clockwork.every(1.hour, 'myjob') 43 | 44 | assert_will_run(t=Time.now) 45 | assert_wont_run(t+30*60) 46 | assert_will_run(t+60*60) 47 | end 48 | 49 | test "once a day at 16:20" do 50 | Clockwork.every(1.day, 'myjob', :at => '16:20') 51 | 52 | assert_wont_run Time.parse('jan 1 2010 16:19:59') 53 | assert_will_run Time.parse('jan 1 2010 16:20:00') 54 | assert_wont_run Time.parse('jan 1 2010 16:20:01') 55 | assert_wont_run Time.parse('jan 2 2010 16:19:59') 56 | assert_will_run Time.parse('jan 2 2010 16:20:00') 57 | end 58 | 59 | test "aborts when no handler defined" do 60 | Clockwork.clear! 61 | assert_raise(Clockwork::NoHandlerDefined) do 62 | Clockwork.every(1.minute, 'myjob') 63 | end 64 | end 65 | 66 | test "aborts when fails to parse" do 67 | assert_raise(Clockwork::FailedToParse) do 68 | Clockwork.every(1.day, "myjob", :at => "a:bc") 69 | end 70 | end 71 | 72 | test "general handler" do 73 | $set_me = 0 74 | Clockwork.handler { $set_me = 1 } 75 | Clockwork.every(1.minute, 'myjob') 76 | Clockwork.tick(Time.now) 77 | assert_equal 1, $set_me 78 | end 79 | 80 | test "event-specific handler" do 81 | $set_me = 0 82 | Clockwork.every(1.minute, 'myjob') { $set_me = 2 } 83 | Clockwork.tick(Time.now) 84 | assert_equal 2, $set_me 85 | end 86 | 87 | test "exceptions are trapped and logged" do 88 | Clockwork.handler { raise 'boom' } 89 | event = Clockwork.every(1.minute, 'myjob') 90 | event.expects(:log_error) 91 | assert_nothing_raised { Clockwork.tick(Time.now) } 92 | end 93 | 94 | test "exceptions still set the last timestamp to avoid spastic error loops" do 95 | Clockwork.handler { raise 'boom' } 96 | event = Clockwork.every(1.minute, 'myjob') 97 | event.stubs(:log_error) 98 | Clockwork.tick(t = Time.now) 99 | assert_equal t, event.last 100 | end 101 | end 102 | --------------------------------------------------------------------------------