clockwork 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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