clockwork 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ Clockwork - a scheduler process to replace cron
2
+ ===============================================
3
+
4
+ Cron is non-ideal for running scheduled application tasks, especially in an app
5
+ deployed to multiple machines. [More details.](http://adam.heroku.com/past/2010/4/13/rethinking_cron/)
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.
12
+
13
+ Example
14
+ -------
15
+
16
+ Create schedule.rb:
17
+
18
+ require 'clockwork'
19
+ include Clockwork
20
+
21
+ every('10s') { puts 'every 10 seconds' }
22
+ every( '3m') { puts 'every 3 minutes' }
23
+ every( '1h') { puts 'once an hour' }
24
+
25
+ every('1d', :at => '00:00') { puts 'every night at midnight' }
26
+
27
+ Run it with the clockwork binary:
28
+
29
+ $ clockwork schedule.rb
30
+
31
+ Or run directly with Ruby:
32
+
33
+ $ ruby -r schedule -e Clockwork.run
34
+
35
+ Use with queueing
36
+ -----------------
37
+
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
42
+ [Delayed Job](http://www.therailsway.com/2009/7/22/do-it-later-with-delayed-job),
43
+ [Beanstalk/Stalker](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/),
44
+ [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
+
49
+ For example, if you're using Beanstalk/Staker:
50
+
51
+ require 'clockwork'
52
+ include Clockwork
53
+
54
+ require 'stalker'
55
+ include Stalker
56
+
57
+ every('1h') { enqueue('feeds.refresh') }
58
+ every('1d', :at => '01:30') { enqueue('reminders.send') }
59
+
60
+ 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
+ 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
+
65
+ require 'config/boot'
66
+ require 'config/environment'
67
+
68
+ require 'clockwork'
69
+ include Clockwork
70
+
71
+ every('1h') { Feed.send_later(:refresh) }
72
+ every('1d', :at => '01:30') { Reminder.send_later(:send_reminders) }
73
+
74
+ In production
75
+ -------------
76
+
77
+ Only one scheduler process should ever be running across your whole application
78
+ deployment. For example, if your app is running on three VPS machines (two app
79
+ servers and one database), your app machines might have the following process
80
+ topography:
81
+
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)
84
+
85
+ You should use Monit, God, Upstart, or Inittab to keep your scheduler process
86
+ running the same way you keep your web and workers running.
87
+
88
+ Meta
89
+ ----
90
+
91
+ Created by Adam Wiggins
92
+
93
+ Inspired by [rufus-scheduler](http://rufus.rubyforge.org/rufus-scheduler/) and [http://github.com/bvandenbos/resque-scheduler](resque-scehduler)
94
+
95
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
96
+
97
+ http://github.com/adamwiggins/clockwork
98
+
data/Rakefile ADDED
@@ -0,0 +1,16 @@
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
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/clockwork ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
4
+ require 'clockwork'
5
+ include Clockwork
6
+
7
+ usage = "clockwork <schedule.rb>"
8
+ file = ARGV.shift or abort usage
9
+
10
+ require file
11
+
12
+ trap('INT') do
13
+ puts "\rExiting"
14
+ exit
15
+ end
16
+
17
+ run
data/lib/clockwork.rb ADDED
@@ -0,0 +1,80 @@
1
+ module Clockwork
2
+ class Event
3
+ def initialize(span, options={}, &block)
4
+ @secs = parse_span(span)
5
+ @at = parse_at(options[:at])
6
+ @last = nil
7
+ @block = block
8
+ end
9
+
10
+ def time?(t)
11
+ ellapsed_ready = (@last.nil? or (t - @last).to_i >= @secs)
12
+ time_ready = (@at.nil? or (t.hour == @at[0] and t.min == @at[1]))
13
+ ellapsed_ready and time_ready
14
+ end
15
+
16
+ def run(t)
17
+ @block.call
18
+ @last = t
19
+ end
20
+
21
+ class FailedToParse < RuntimeError; end
22
+
23
+ def parse_span(span)
24
+ m = span.match(/^(\d+)([smhd])$/)
25
+ raise FailedToParse, span unless m
26
+ ordinal, magnitude = m[1].to_i, m[2]
27
+ ordinal * magnitude_multiplier[magnitude]
28
+ end
29
+
30
+ def magnitude_multiplier
31
+ {
32
+ 's' => 1,
33
+ 'm' => 60,
34
+ 'h' => 60*60,
35
+ 'd' => 24*60*60
36
+ }
37
+ end
38
+
39
+ def parse_at(at)
40
+ return unless at
41
+ m = at.match(/^(\d\d):(\d\d)$/)
42
+ raise FailedToParse, at unless m
43
+ hour, min = m[1].to_i, m[2].to_i
44
+ raise FailedToParse, at if hour >= 24 or min >= 60
45
+ [ hour, min ]
46
+ end
47
+ end
48
+
49
+ extend self
50
+
51
+ def every(span, options={}, &block)
52
+ event = Event.new(span, options, &block)
53
+ @@events ||= []
54
+ @@events << event
55
+ event
56
+ end
57
+
58
+ def run
59
+ loop do
60
+ tick
61
+ sleep 1
62
+ end
63
+ end
64
+
65
+ def tick(t=Time.now)
66
+ to_run = @@events.select do |event|
67
+ event.time?(t)
68
+ end
69
+
70
+ to_run.each do |event|
71
+ event.run(t)
72
+ end
73
+
74
+ to_run
75
+ end
76
+
77
+ def clear!
78
+ @@events = []
79
+ end
80
+ end
@@ -0,0 +1,50 @@
1
+ require File.dirname(__FILE__) + '/../lib/clockwork'
2
+ require 'contest'
3
+
4
+ class ClockworkTest < Test::Unit::TestCase
5
+ setup do
6
+ Clockwork.clear!
7
+ end
8
+
9
+ def assert_will_run(t)
10
+ assert_equal 1, Clockwork.tick(t).size
11
+ end
12
+
13
+ def assert_wont_run(t)
14
+ assert_equal 0, Clockwork.tick(t).size
15
+ end
16
+
17
+ test "once a minute" do
18
+ Clockwork.every('1m') { }
19
+
20
+ assert_will_run(t=Time.now)
21
+ assert_wont_run(t+30)
22
+ assert_will_run(t+60)
23
+ end
24
+
25
+ test "every three minutes" do
26
+ Clockwork.every('3m') { }
27
+
28
+ assert_will_run(t=Time.now)
29
+ assert_wont_run(t+2*60)
30
+ assert_will_run(t+3*60)
31
+ end
32
+
33
+ test "once an hour" do
34
+ Clockwork.every('1h') { }
35
+
36
+ assert_will_run(t=Time.now)
37
+ assert_wont_run(t+30*60)
38
+ assert_will_run(t+60*60)
39
+ end
40
+
41
+ test "once a day at 16:20" do
42
+ Clockwork.every('1d', :at => '16:20') { }
43
+
44
+ assert_wont_run Time.parse('jan 1 2010 16:19:59')
45
+ assert_will_run Time.parse('jan 1 2010 16:20:00')
46
+ assert_wont_run Time.parse('jan 1 2010 16:20:01')
47
+ assert_wont_run Time.parse('jan 2 2010 16:19:59')
48
+ assert_will_run Time.parse('jan 2 2010 16:20:00')
49
+ end
50
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clockwork
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Adam Wiggins
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-11 00:00:00 -07:00
18
+ default_executable: clockwork
19
+ dependencies: []
20
+
21
+ 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.
22
+ email: adam@heroku.com
23
+ executables:
24
+ - clockwork
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.md
29
+ files:
30
+ - README.md
31
+ - Rakefile
32
+ - VERSION
33
+ - bin/clockwork
34
+ - lib/clockwork.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/adamwiggins/clockwork
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --charset=UTF-8
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ segments:
49
+ - 0
50
+ version: "0"
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ version: "0"
58
+ requirements: []
59
+
60
+ rubyforge_project: clockwork
61
+ rubygems_version: 1.3.6
62
+ signing_key:
63
+ specification_version: 3
64
+ summary: A scheduler process to replace cron.
65
+ test_files:
66
+ - test/clockwork_test.rb