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 +98 -0
- data/Rakefile +16 -0
- data/VERSION +1 -0
- data/bin/clockwork +17 -0
- data/lib/clockwork.rb +80 -0
- data/test/clockwork_test.rb +50 -0
- metadata +66 -0
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
|