daeltar-clockwork 0.2.4

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.
Files changed (6) hide show
  1. data/README.md +142 -0
  2. data/Rakefile +23 -0
  3. data/VERSION +1 -0
  4. data/bin/clockwork +20 -0
  5. data/lib/clockwork.rb +123 -0
  6. metadata +69 -0
data/README.md ADDED
@@ -0,0 +1,142 @@
1
+ Clockwork - a clock 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 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.
13
+
14
+ Quickstart
15
+ ----------
16
+
17
+ Create clock.rb:
18
+
19
+ require 'clockwork'
20
+ include Clockwork
21
+
22
+ handler do |job|
23
+ puts "Running #{job}"
24
+ end
25
+
26
+ every(10.seconds, 'frequent.job')
27
+ every(3.minutes, 'less.frequent.job')
28
+ every(1.hour, 'hourly.job')
29
+
30
+ every(1.day, 'midnight.job', :at => '00:00')
31
+
32
+ Run it with the clockwork binary:
33
+
34
+ $ clockwork clock.rb
35
+ Starting clock for 4 events: [ frequent.job less.frequent.job hourly.job midnight.job ]
36
+ Triggering frequent.job
37
+
38
+ Use with queueing
39
+ -----------------
40
+
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
45
+ [Delayed Job](http://www.therailsway.com/2009/7/22/do-it-later-with-delayed-job),
46
+ [Beanstalk/Stalker](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/),
47
+ [RabbitMQ/Minion](http://adamblog.heroku.com/past/2009/9/28/background_jobs_with_rabbitmq_and_minion/), or
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.
51
+
52
+ For example, if you're using Beanstalk/Staker:
53
+
54
+ require 'stalker'
55
+
56
+ handler { |job| Stalker.enqueue(job) }
57
+
58
+ every(1.hour, 'feeds.refresh')
59
+ every(1.day, 'reminders.send', :at => '01:30')
60
+
61
+ Using a queueing system which doesn't require that your full application be
62
+ loaded is preferable, because the clock process can keep a tiny memory
63
+ footprint. If you're using DJ or Resque, however, you can go ahead and load
64
+ your full application enviroment, and use per-event blocks to call DJ or Resque
65
+ enqueue methods. For example, with DJ/Rails:
66
+
67
+ require 'config/boot'
68
+ require 'config/environment'
69
+
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.
80
+
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
113
+
114
+ In production
115
+ -------------
116
+
117
+ Only one clock process should ever be running across your whole application
118
+ deployment. For example, if your app is running on three VPS machines (two app
119
+ servers and one database), your app machines might have the following process
120
+ topography:
121
+
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)
124
+
125
+ You should use Monit, God, Upstart, or Inittab to keep your clock process
126
+ running the same way you keep your web and workers running.
127
+
128
+ Meta
129
+ ----
130
+
131
+ Created by Adam Wiggins
132
+
133
+ Inspired by [rufus-scheduler](http://rufus.rubyforge.org/rufus-scheduler/) and [http://github.com/bvandenbos/resque-scheduler](resque-scehduler)
134
+
135
+ Design assistance from Peter van Hardenberg and Matthew Soldo
136
+
137
+ Patches contributed by Mark McGranaghan
138
+
139
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
140
+
141
+ http://github.com/adamwiggins/clockwork
142
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'bundler/setup'
2
+ require 'jeweler'
3
+
4
+ Jeweler::Tasks.new do |s|
5
+ s.name = "daeltar-clockwork"
6
+ s.summary = "A scheduler process to replace cron."
7
+ 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."
8
+ s.author = "Adam Wiggins"
9
+ s.email = "adam@heroku.com"
10
+ s.homepage = "http://github.com/adamwiggins/clockwork"
11
+ s.executables = [ "clockwork" ]
12
+ s.rubyforge_project = "daeltar-clockwork"
13
+
14
+ s.files = FileList["[A-Z]*", "{bin,lib}/**/*"]
15
+ end
16
+
17
+ Jeweler::GemcutterTasks.new
18
+
19
+ task 'test' do
20
+ sh "ruby test/clockwork_test.rb"
21
+ end
22
+
23
+ task :build => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.4
data/bin/clockwork ADDED
@@ -0,0 +1,20 @@
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 <clock.rb>"
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
data/lib/clockwork.rb ADDED
@@ -0,0 +1,123 @@
1
+ module Clockwork
2
+ class FailedToParse < StandardError; end;
3
+
4
+ class ClockworkEvent
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 = ClockworkEvent.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
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: daeltar-clockwork
3
+ version: !ruby/object:Gem::Version
4
+ hash: 31
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 4
10
+ version: 0.2.4
11
+ platform: ruby
12
+ authors:
13
+ - Adam Wiggins
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-07-30 00:00:00 Z
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
+ homepage: http://github.com/adamwiggins/clockwork
36
+ licenses: []
37
+
38
+ post_install_message:
39
+ rdoc_options: []
40
+
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ hash: 3
49
+ segments:
50
+ - 0
51
+ version: "0"
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ requirements: []
62
+
63
+ rubyforge_project: daeltar-clockwork
64
+ rubygems_version: 1.8.6
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: A scheduler process to replace cron.
68
+ test_files: []
69
+