clockwork 0.1.1 → 0.2.0

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