clockworkd 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2011 Artem Rufanov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README ADDED
@@ -0,0 +1,89 @@
1
+ Clockworkd - 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
+ Clockworkd 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
+ Configure application:
18
+
19
+ In your Gemfile gem "clockworkd", ">= 0.2.5"
20
+ Run generator to generate config yml and script file
21
+ Use config/initiaizers to configure options for this gem
22
+
23
+
24
+ Modify clockworkd.yml:
25
+
26
+ # Run job every two minute: */2 * * * *
27
+ clear_sesssion_job:
28
+ cron: "59 1 * * *"
29
+ block: Delayed::Job.enqueue ClearSessionsJob.new
30
+ description: "This job clear sessions table"
31
+
32
+ Run it as console or daemon application (rails 3.x):
33
+
34
+ $ ruby script/clockworkd run
35
+ $ ruby script/clockworkd --identifier=0 start
36
+
37
+ Use clockworkd with capistrano:
38
+
39
+ after "deploy:stop", "clockworkd:stop"
40
+ after "deploy:start", "clockworkd:start"
41
+ after "deploy:restart", "clockworkd:restart"
42
+
43
+ Use monit script to monitor it:
44
+
45
+ check process clockworkd
46
+ with pidfile /var/www/apps/{app_name}/shared/pids/clockworkd.pid
47
+ start program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/clockworkd start"
48
+ stop program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/clockworkd stop"
49
+
50
+
51
+
52
+ In production
53
+ =======
54
+
55
+ Only one clock process should ever be running across your whole application
56
+ deployment. For example, if your app is running on three VPS machines (two app
57
+ servers and one database), your app machines might have the following process
58
+ topography:
59
+
60
+ * App server 1: 3 web (thin start), 3 workers (rake jobs:work), 1 clock (clockwork clock.rb)
61
+ * App server 2: 3 web (thin start), 3 workers (rake jobs:work)
62
+
63
+ You should use Monit, God, Upstart, or Inittab to keep your clock process
64
+ running the same way you keep your web and workers running.
65
+
66
+ Meta
67
+ =======
68
+
69
+ Inspired by
70
+
71
+ * [clockwork] (http://github.com/adamwiggins/clockwork)
72
+ * [delayed_job] (https://github.com/collectiveidea/delayed_job)
73
+ * [rufus-scheduler](http://rufus.rubyforge.org/rufus-scheduler/)
74
+ * [resque-scehduler] (http://github.com/bvandenbos/resque-scheduler)
75
+
76
+
77
+ Installation
78
+ =======
79
+
80
+ * Type 'gem install --local clockworkd' with root account if you have installed RubyGems.
81
+
82
+
83
+ Example
84
+ =======
85
+
86
+ Example goes here.
87
+
88
+ Copyright (c) 2011 arufanov, released under the MIT license.
89
+
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'jeweler'
2
+
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "clockworkd"
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/arufanov/clockworkd"
10
+ s.executables = [ "clockworkd" ]
11
+ s.rubyforge_project = "clockworkd"
12
+
13
+ s.files = FileList["[A-Z]*", "{bin,lib}/**/*"]
14
+ end
15
+
16
+ Jeweler::GemcutterTasks.new
17
+
18
+ task 'test' do
19
+ sh "ruby spec/lib/clockworkd_spec.rb"
20
+ end
21
+
22
+ task :build => :test
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.5
data/changelog ADDED
@@ -0,0 +1,23 @@
1
+ Introduction:
2
+ To see the latest list of the change log please visit the Change Log page at www.majoron.com.
3
+
4
+ Legend:
5
+ Follow notation is used at change log, roadmap and known bugs. Each bug begins with a version,
6
+ then follow category of the bug inside {}. It can be bug report, feature request and etc.
7
+ Then follow component inside []. After follow bug number at bug tracking system between // signs.
8
+ And then follow a short description of the bug.
9
+
10
+ Example:
11
+ For example bug: "1.0 { Feature Request } [ AntHill ] / 380 / STLport support required" means
12
+ that bug was created for 1.0 version of the AntHill component, bug is feature request with
13
+ 380 number at bug tracking system. And bug requires STLPort support implementation.
14
+
15
+ Version 0.2
16
+ -----------
17
+ 0.2 { Bug Report } [ Clockworkd ] / X / Add before_fork & after fork hook to restore ActiveRecord connection after fork
18
+ 0.2 { Bug Report } [ Clockworkd ] / X / Support rails 2.5.x
19
+ 0.2 { Bug Report } [ Clockworkd ] / X / Generator that install clockwork to script was added
20
+ 0.2 { Bug Report } [ Clockworkd ] / X / Capistrano receips was added
21
+ 0.2 { Bug Report } [ Clockworkd ] / X / Monit script was added
22
+ 0.2 { Bug Report } [ Clockworkd ] / X / Ability to run as daemons was added
23
+
@@ -0,0 +1,36 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{clockworkd}
8
+ s.version = "0.2.5"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Adam Wiggins"]
12
+ s.date = %q{2011-07-31}
13
+ s.description = %q{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.}
14
+ s.email = %q{adam@heroku.com}
15
+ s.extra_rdoc_files = [
16
+ "README"
17
+ ]
18
+ s.files = Dir.glob('**/*') - Dir.glob('distrib/**/*') - Dir.glob('lib/api/**/*') - Dir.glob('doc/*.xpr')
19
+ s.homepage = %q{http://www.majoron.com/project/rbundle/clockworkd}
20
+ s.require_paths = ["lib"]
21
+ s.rubyforge_project = %q{clockworkd}
22
+ s.rubygems_version = %q{1.5.0}
23
+ s.summary = %q{A scheduler process to replace cron.}
24
+ s.test_files = Dir.glob('spec/**/*')
25
+ s.add_runtime_dependency 'tzinfo', '>= 0.3.23'
26
+
27
+ if s.respond_to? :specification_version then
28
+ s.specification_version = 3
29
+
30
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
31
+ else
32
+ end
33
+ else
34
+ end
35
+ end
36
+
@@ -0,0 +1,14 @@
1
+ # an example Monit configuration file for clockworkd
2
+ # See: http://stackoverflow.com/questions/1226302/how-to-monitor-delayedjob-with-monit/1285611
3
+ #
4
+ # To use:
5
+ # 1. copy to /var/www/apps/{app_name}/shared/clockworkd.monitrc
6
+ # 2. replace {app_name} as appropriate
7
+ # 3. add this to your /etc/monit/monitrc
8
+ #
9
+ # include /var/www/apps/{app_name}/shared/clockworkd.monitrc
10
+
11
+ check process clockworkd
12
+ with pidfile /var/www/apps/{app_name}/shared/pids/clockworkd.pid
13
+ start program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/clockworkd start"
14
+ stop program = "/usr/bin/env RAILS_ENV=production /var/www/apps/{app_name}/current/script/clockworkd stop"
@@ -0,0 +1,12 @@
1
+ class ClockworkdGenerator < Rails::Generator::Base
2
+ default_options :skip_migration => false
3
+
4
+ def manifest
5
+ record do |m|
6
+ m.template 'script', 'script/clockworkd', :chmod => 0755
7
+ m.template 'clockworkd.yml', 'config/clockworkd.yml'
8
+ end
9
+ end
10
+
11
+
12
+ end
@@ -0,0 +1,14 @@
1
+
2
+ # Run job every day at 1:59: 59 1 * * *
3
+ # Run job every two minute: */2 * * * *
4
+ clear_sesssion_job:
5
+ cron: "59 1 * * *"
6
+ block: Delayed::Job.enqueue ClearSessionsJob.new
7
+ description: "This job clear sessions table"
8
+
9
+ # Run job every day at 4:59: 59 4 * * *
10
+ # Run job every two minute: */2 * * * *
11
+ clear_job_reports_job:
12
+ cron: "59 4 * * *"
13
+ block: Delayed::Job.enqueue ClearJobReportsJob.new
14
+ description: "This job clear job report table"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
4
+ require 'clockworkd/command'
5
+ Clockworkd::Command.new(ARGV).daemonize
data/knownbugs ADDED
@@ -0,0 +1,18 @@
1
+ Introduction:
2
+ To see the latest list of the known bugs please visit the Known bugs page at www.majoron.com.
3
+
4
+ Legend:
5
+ Follow notation is used at change log, roadmap and known bugs. Each bug begins with a version,
6
+ then follow category of the bug inside {}. It can be bug report, feature request and etc.
7
+ Then follow component inside []. After follow bug number at bug tracking system between // signs.
8
+ And then follow a short description of the bug.
9
+
10
+ Example:
11
+ For example bug: "1.0 { Feature Request } [ AntHill ] / 380 / STLport support required" means
12
+ that bug was created for 1.0 version of the AntHill component, bug is feature request with
13
+ 380 number at bug tracking system. And bug requires STLPort support implementation.
14
+
15
+ Version 0.2
16
+ -----------
17
+ 0.2 { Bug Report } [ Clockworkd ] / X / There isn't known bugs
18
+
data/lib/clockworkd.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Include files
2
+ require File.dirname(__FILE__) + '/clockworkd/cronline'
3
+ require File.dirname(__FILE__) + '/clockworkd/event'
4
+ require File.dirname(__FILE__) + '/clockworkd/worker'
5
+ require File.dirname(__FILE__) + '/clockworkd/command'
6
+
7
+ unless 1.respond_to?(:seconds)
8
+ class Numeric
9
+ def seconds; self; end
10
+ alias :second :seconds
11
+
12
+ def minutes; self * 60; end
13
+ alias :minute :minutes
14
+
15
+ def hours; self * 3600; end
16
+ alias :hour :hours
17
+
18
+ def days; self * 86400; end
19
+ alias :day :days
20
+ end
21
+ end
@@ -0,0 +1,90 @@
1
+ require 'rubygems'
2
+ require 'daemons'
3
+ require 'optparse'
4
+ require 'logger'
5
+
6
+ module Clockworkd
7
+ class Command
8
+
9
+ def initialize(args)
10
+ @files_to_reopen = []
11
+ @options = {
12
+ :quiet => true,
13
+ :identifier => 0,
14
+ :pid_dir => "#{Rails.root}/tmp/pids",
15
+ :clock_file => "#{Rails.root}/config/clockworkd.yml"
16
+ }
17
+
18
+ @monitor = false
19
+
20
+ opts = OptionParser.new do |opts|
21
+ opts.banner = "Usage: #{File.basename($0)} [options] start|stop|restart|run"
22
+
23
+ opts.on('-h', '--help', 'Show this message') do
24
+ puts opts
25
+ exit 1
26
+ end
27
+ opts.on('--clock-file=FILE', 'Specifies an alternate clockwork file with schuduling.') do |file|
28
+ @options[:clock_file] = file
29
+ end
30
+ opts.on('--pid-dir=DIR', 'Specifies an alternate directory in which to store the process ids.') do |dir|
31
+ @options[:pid_dir] = dir
32
+ end
33
+ opts.on('-i', '--identifier=n', 'A numeric identifier for the clockwork process.') do |n|
34
+ @options[:identifier] = n
35
+ end
36
+ opts.on('-m', '--monitor', 'Start monitor process.') do
37
+ @monitor = true
38
+ end
39
+ opts.on('--sleep-delay N', "Amount of time to sleep when no events are found") do |n|
40
+ @options[:sleep_delay] = n
41
+ end
42
+ end
43
+ @args = opts.parse!(args)
44
+ end
45
+
46
+ def daemonize
47
+ Clockworkd::Worker.before_fork
48
+
49
+ ObjectSpace.each_object(File) do |file|
50
+ @files_to_reopen << file unless file.closed?
51
+ end
52
+
53
+ dir = @options[:pid_dir]
54
+ Dir.mkdir(dir) unless File.exists?(dir)
55
+
56
+ process_name = "clockworkd.#{@options[:identifier]}"
57
+ run_process(process_name, dir)
58
+ end
59
+
60
+ def run_process(process_name, dir)
61
+ Daemons.run_proc(process_name, :multiple => false, :dir => dir, :dir_mode => :normal, :monitor => @monitor, :ARGV => @args) do |*args|
62
+ run process_name
63
+ end
64
+ end
65
+
66
+ def run(worker_name = nil)
67
+ Dir.chdir(Rails.root)
68
+
69
+ # Re-open file handles
70
+ @files_to_reopen.each do |file|
71
+ begin
72
+ file.reopen file.path, "a+"
73
+ file.sync = true
74
+ rescue ::Exception => e
75
+ raise e
76
+ end
77
+ end
78
+
79
+ Clockworkd::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'clockworkd.log'))
80
+ Clockworkd::Worker.after_fork
81
+
82
+ Clockworkd::Worker.new(@options).run
83
+ rescue => e
84
+ Rails.logger.fatal e
85
+ STDERR.puts e.message
86
+ exit 1
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,310 @@
1
+ #--
2
+ # Copyright (c) 2006-2011, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+ require 'tzinfo'
26
+
27
+
28
+ module Clockworkd
29
+
30
+ #
31
+ # A 'cron line' is a line in the sense of a crontab
32
+ # (man 5 crontab) file line.
33
+ #
34
+ class CronLine
35
+
36
+ # The string used for creating this cronline instance.
37
+ #
38
+ attr_reader :original
39
+
40
+ attr_reader :seconds
41
+ attr_reader :minutes
42
+ attr_reader :hours
43
+ attr_reader :days
44
+ attr_reader :months
45
+ attr_reader :weekdays
46
+ attr_reader :monthdays
47
+ attr_reader :timezone
48
+
49
+ def initialize(line)
50
+
51
+ super()
52
+
53
+ @original = line
54
+
55
+ items = line.split
56
+
57
+ @timezone = (TZInfo::Timezone.get(items.last) rescue nil)
58
+ items.pop if @timezone
59
+
60
+ raise ArgumentError.new(
61
+ "not a valid cronline : '#{line}'"
62
+ ) unless items.length == 5 or items.length == 6
63
+
64
+ offset = items.length - 5
65
+
66
+ @seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
67
+ @minutes = parse_item(items[0 + offset], 0, 59)
68
+ @hours = parse_item(items[1 + offset], 0, 24)
69
+ @days = parse_item(items[2 + offset], 1, 31)
70
+ @months = parse_item(items[3 + offset], 1, 12)
71
+ @weekdays, @monthdays = parse_weekdays(items[4 + offset])
72
+ end
73
+
74
+ # Returns true if the given time matches this cron line.
75
+ #
76
+ def matches?(time)
77
+
78
+ time = Time.at(time) unless time.kind_of?(Time)
79
+
80
+ time = @timezone.utc_to_local(time.getutc) if @timezone
81
+
82
+ return false unless sub_match?(time.sec, @seconds)
83
+ return false unless sub_match?(time.min, @minutes)
84
+ return false unless sub_match?(time.hour, @hours)
85
+ return false unless date_match?(time)
86
+ true
87
+ end
88
+
89
+ # Returns the next time that this cron line is supposed to 'fire'
90
+ #
91
+ # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
92
+ # (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
93
+ #
94
+ # This method accepts an optional Time parameter. It's the starting point
95
+ # for the 'search'. By default, it's Time.now
96
+ #
97
+ # Note that the time instance returned will be in the same time zone that
98
+ # the given start point Time (thus a result in the local time zone will
99
+ # be passed if no start time is specified (search start time set to
100
+ # Time.now))
101
+ #
102
+ # Rufus::CronLine.new('30 7 * * *').next_time(
103
+ # Time.mktime(2008, 10, 24, 7, 29))
104
+ # #=> Fri Oct 24 07:30:00 -0500 2008
105
+ #
106
+ # Rufus::CronLine.new('30 7 * * *').next_time(
107
+ # Time.utc(2008, 10, 24, 7, 29))
108
+ # #=> Fri Oct 24 07:30:00 UTC 2008
109
+ #
110
+ # Rufus::CronLine.new('30 7 * * *').next_time(
111
+ # Time.utc(2008, 10, 24, 7, 29)).localtime
112
+ # #=> Fri Oct 24 02:30:00 -0500 2008
113
+ #
114
+ # (Thanks to K Liu for the note and the examples)
115
+ #
116
+ def next_time(now=Time.now)
117
+
118
+ time = @timezone ? @timezone.utc_to_local(now.getutc) : now
119
+
120
+ time = time - time.usec * 1e-6 + 1
121
+ # little adjustment before starting
122
+
123
+ loop do
124
+
125
+ unless date_match?(time)
126
+ time += (24 - time.hour) * 3600 - time.min * 60 - time.sec
127
+ next
128
+ end
129
+ unless sub_match?(time.hour, @hours)
130
+ time += (60 - time.min) * 60 - time.sec
131
+ next
132
+ end
133
+ unless sub_match?(time.min, @minutes)
134
+ time += 60 - time.sec
135
+ next
136
+ end
137
+ unless sub_match?(time.sec, @seconds)
138
+ time += 1
139
+ next
140
+ end
141
+
142
+ break
143
+ end
144
+
145
+ if @timezone
146
+ time = @timezone.local_to_utc(time)
147
+ time = time.getlocal unless now.utc?
148
+ end
149
+
150
+ time
151
+ end
152
+
153
+ # Returns an array of 6 arrays (seconds, minutes, hours, days,
154
+ # months, weekdays).
155
+ # This method is used by the cronline unit tests.
156
+ #
157
+ def to_array
158
+
159
+ [
160
+ @seconds,
161
+ @minutes,
162
+ @hours,
163
+ @days,
164
+ @months,
165
+ @weekdays,
166
+ @monthdays,
167
+ @timezone ? @timezone.name : nil
168
+ ]
169
+ end
170
+
171
+ private
172
+
173
+ WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
174
+
175
+ def parse_weekdays(item)
176
+
177
+ return nil if item == '*'
178
+
179
+ items = item.downcase.split(',')
180
+
181
+ weekdays = nil
182
+ monthdays = nil
183
+
184
+ items.each do |it|
185
+
186
+ if it.match(/#[12345]$/)
187
+
188
+ raise ArgumentError.new(
189
+ "ranges are not supported for monthdays (#{it})"
190
+ ) if it.index('-')
191
+
192
+ (monthdays ||= []) << it
193
+ else
194
+
195
+ WEEKDAYS.each_with_index { |a, i| it.gsub!(/#{a}/, i.to_s) }
196
+
197
+ its = it.index('-') ? parse_range(it, 0, 7) : [ Integer(it) ]
198
+ its = its.collect { |i| i == 7 ? 0 : i }
199
+
200
+ (weekdays ||= []).concat(its)
201
+ end
202
+ end
203
+
204
+ weekdays = weekdays.uniq if weekdays
205
+
206
+ [ weekdays, monthdays ]
207
+ end
208
+
209
+ def parse_item(item, min, max)
210
+
211
+ return nil if item == '*'
212
+ return parse_list(item, min, max) if item.index(',')
213
+ return parse_range(item, min, max) if item.index('*') or item.index('-')
214
+
215
+ i = Integer(item)
216
+
217
+ i = min if i < min
218
+ i = max if i > max
219
+
220
+ [ i ]
221
+ end
222
+
223
+ def parse_list(item, min, max)
224
+
225
+ item.split(',').inject([]) { |r, i|
226
+ r.push(parse_range(i, min, max))
227
+ }.flatten
228
+ end
229
+
230
+ def parse_range(item, min, max)
231
+
232
+ i = item.index('-')
233
+ j = item.index('/')
234
+
235
+ return item.to_i if (not i and not j)
236
+
237
+ inc = j ? Integer(item[j+1..-1]) : 1
238
+
239
+ istart = -1
240
+ iend = -1
241
+
242
+ if i
243
+
244
+ istart = Integer(item[0..i - 1])
245
+
246
+ if j
247
+ iend = Integer(item[i + 1..j - 1])
248
+ else
249
+ iend = Integer(item[i + 1..-1])
250
+ end
251
+
252
+ else # case */x
253
+
254
+ istart = min
255
+ iend = max
256
+ end
257
+
258
+ istart = min if istart < min
259
+ iend = max if iend > max
260
+
261
+ result = []
262
+
263
+ value = istart
264
+ loop do
265
+ result << value
266
+ value = value + inc
267
+ break if value > iend
268
+ end
269
+
270
+ result
271
+ end
272
+
273
+ def sub_match?(value, values)
274
+
275
+ values.nil? || values.include?(value)
276
+ end
277
+
278
+ def monthday_match(monthday, monthdays)
279
+
280
+ return true if monthdays == nil
281
+ return true if monthdays.include?(monthday)
282
+ end
283
+
284
+ def date_match?(date)
285
+
286
+ return false unless sub_match?(date.day, @days)
287
+ return false unless sub_match?(date.month, @months)
288
+ return false unless sub_match?(date.wday, @weekdays)
289
+ return false unless sub_match?(CronLine.monthday(date), @monthdays)
290
+ true
291
+ end
292
+
293
+ DAY_IN_SECONDS = 7 * 24 * 3600
294
+
295
+ def self.monthday(date)
296
+
297
+ count = 1
298
+ date2 = date.dup
299
+
300
+ loop do
301
+ date2 = date2 - DAY_IN_SECONDS
302
+ break if date2.month != date.month
303
+ count = count + 1
304
+ end
305
+
306
+ "#{WEEKDAYS[date.wday]}##{count}"
307
+ end
308
+ end
309
+ end
310
+
@@ -0,0 +1,33 @@
1
+ module Clockworkd
2
+ class Event
3
+ attr_accessor :job, :block, :cronline, :cron_time, :next_time
4
+
5
+ def initialize(job, block, cronline)
6
+ @job = job
7
+ @block = block
8
+ @cronline = cronline
9
+ @cron_time = CronLine.new(@cronline)
10
+ @next_time = @cron_time.next_time
11
+ end
12
+
13
+ def to_s
14
+ @job = job
15
+ end
16
+
17
+ def time?(t)
18
+ if (t.to_i - @next_time.to_i) > 0
19
+ @next_time = @cron_time.next_time
20
+ return true
21
+ end
22
+ false
23
+ end
24
+
25
+ def run(t)
26
+ eval(@block)
27
+ rescue Exception => e
28
+ raise e
29
+ end
30
+
31
+ end
32
+ end
33
+
@@ -0,0 +1,50 @@
1
+ # Capistrano Recipes for managing clockworkd
2
+ #
3
+ # Add these callbacks to have the clockworkd process restart when the server
4
+ # is restarted:
5
+ #
6
+ # after "deploy:stop", "clockworkd:stop"
7
+ # after "deploy:start", "clockworkd:start"
8
+ # after "deploy:restart", "clockworkd:restart"
9
+ #
10
+ # If you want to use command line options, for example to start multiple workers,
11
+ # define a Capistrano variable clockworkd_args:
12
+ #
13
+ # set :clockworkd_args, "-n 2"
14
+ #
15
+ # If you've got clockworkd workers running on a servers, you can also specify
16
+ # which servers have clockworkd running and should be restarted after deploy.
17
+ #
18
+ # set :clockworkd_server_role, :worker
19
+ #
20
+
21
+ Capistrano::Configuration.instance.load do
22
+ namespace :clockworkd do
23
+ def rails_env
24
+ fetch(:rails_env, false) ? "RAILS_ENV=#{fetch(:rails_env)}" : ''
25
+ end
26
+
27
+ def args
28
+ fetch(:clockworkd_args, "")
29
+ end
30
+
31
+ def roles
32
+ fetch(:clockworkd_server_role, :app)
33
+ end
34
+
35
+ desc "Stop the clockworkd process"
36
+ task :stop, :roles => lambda { roles } do
37
+ run "cd #{current_path};#{rails_env} script/clockworkd stop"
38
+ end
39
+
40
+ desc "Start the clockworkd process"
41
+ task :start, :roles => lambda { roles } do
42
+ run "cd #{current_path};#{rails_env} script/clockworkd start #{args}"
43
+ end
44
+
45
+ desc "Restart the clockworkd process"
46
+ task :restart, :roles => lambda { roles } do
47
+ run "cd #{current_path};#{rails_env} script/clockworkd restart #{args}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,102 @@
1
+ require 'logger'
2
+
3
+ module Clockworkd
4
+ class Worker
5
+ cattr_accessor :clock_file, :sleep_delay, :logger
6
+ self.clock_file = "#{::Rails.root}/config/clockworkd.yml"
7
+ self.sleep_delay = 5
8
+
9
+ self.logger = if defined?(::Rails)
10
+ ::Rails.logger
11
+ elsif defined?(RAILS_DEFAULT_LOGGER)
12
+ RAILS_DEFAULT_LOGGER
13
+ end
14
+
15
+ # name_prefix is ignored if name is set directly
16
+ attr_accessor :events
17
+
18
+ def initialize(options={})
19
+ self.events = []
20
+
21
+ @quiet = options.has_key?(:quiet) ? options[:quiet] : true
22
+ self.class.clock_file = options[:clock_file] if options.has_key?(:clock_file)
23
+ self.class.sleep_delay = options[:sleep_delay] if options.has_key?(:sleep_delay)
24
+ end
25
+
26
+ # Hook method that is called before a new worker is forked
27
+ def self.before_fork
28
+ ::ActiveRecord::Base.clear_all_connections!
29
+ end
30
+
31
+ # Hook method that is called after a new worker is forked
32
+ def self.after_fork
33
+ ::ActiveRecord::Base.establish_connection
34
+ end
35
+
36
+ # Every worker has a unique name which by default is the pid of the process. There are some
37
+ # advantages to overriding this with something which survives worker retarts: Workers can#
38
+ # safely resume working on tasks which are locked by themselves. The worker will assume that
39
+ # it crashed before.
40
+ def name
41
+ return @name unless @name.nil?
42
+ "host:#{Socket.gethostname} pid:#{Process.pid}" rescue "pid:#{Process.pid}"
43
+ end
44
+
45
+ # Sets the name of the worker.
46
+ # Setting the name to nil will reset the default worker name
47
+ def name=(val)
48
+ @name = val
49
+ end
50
+
51
+
52
+ def run
53
+ trap('TERM') { log 'Exiting...'; $exit = true }
54
+ trap('INT') { log 'Exiting...'; $exit = true }
55
+
56
+ # Load scheduling
57
+ log "Load file with scheduling #{self.class.clock_file.to_s}"
58
+ yml_file = YAML::load(File.open(self.class.clock_file))
59
+ yml_file.each do |key, value|
60
+ job = key
61
+ block = value["block"]
62
+ cronline = value["cron"]
63
+ self.events << Event.new(job, block, cronline)
64
+ end
65
+
66
+ # Start a processing
67
+ log "Starting clock for #{self.events.size} events: [ " + self.events.map { |e| e.to_s }.join(' ') + " ]"
68
+ loop do
69
+ tick
70
+ break if $exit
71
+ sleep(self.class.sleep_delay)
72
+ break if $exit
73
+ end
74
+ end
75
+
76
+ def tick(t=Time.now)
77
+ to_run = self.events.select do |event|
78
+ event.time?(t)
79
+ end
80
+
81
+ to_run.each do |event|
82
+ log "Triggering #{event} with cronline #{event.cronline} and next time: #{event.next_time}"
83
+ begin
84
+ event.run(t)
85
+ rescue Exception => e
86
+ log "Unable to execute #{event} with error: #{e.to_s}", Logger::ERROR
87
+ raise e
88
+ end
89
+ end
90
+
91
+ to_run
92
+ end
93
+
94
+ def log(text, level = Logger::INFO)
95
+ text = "[Worker(#{name})] #{text}"
96
+ puts text unless @quiet
97
+ logger.add level, "#{Time.now.strftime('%FT%T%z')}: #{text}" if logger
98
+ end
99
+
100
+
101
+ end
102
+ end
@@ -0,0 +1,21 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ class ClockworkdGenerator < Rails::Generators::Base
5
+
6
+ include Rails::Generators::Migration
7
+
8
+ def self.source_root
9
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
10
+ end
11
+
12
+ def create_script_file
13
+ template 'script', 'script/clockworkd'
14
+ chmod 'script/clockworkd', 0755
15
+ end
16
+
17
+ def create_clockworkd_file
18
+ template 'clockworkd.yml', 'config/clockworkd.yml'
19
+ end
20
+
21
+ end
@@ -0,0 +1,14 @@
1
+
2
+ # Run job every day at 1:59: 59 1 * * *
3
+ # Run job every two minute: */2 * * * *
4
+ clear_sesssion_job:
5
+ cron: "59 1 * * *"
6
+ block: Delayed::Job.enqueue ClearSessionsJob.new
7
+ description: "This job clear sessions table"
8
+
9
+ # Run job every day at 4:59: 59 4 * * *
10
+ # Run job every two minute: */2 * * * *
11
+ clear_job_reports_job:
12
+ cron: "59 4 * * *"
13
+ block: Delayed::Job.enqueue ClearJobReportsJob.new
14
+ description: "This job clear job report table"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment'))
4
+ require 'clockworkd/command'
5
+ Clockworkd::Command.new(ARGV).daemonize
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'clockworkd', 'recipes'))
data/roadmap ADDED
@@ -0,0 +1,17 @@
1
+ Introduction:
2
+ To see the latest list of the roadmap please visit the Roadmap page at www.majoron.com.
3
+
4
+ Legend:
5
+ Follow notation is used at change log, roadmap and known bugs. Each bug begins with a version,
6
+ then follow category of the bug inside {}. It can be bug report, feature request and etc.
7
+ Then follow component inside []. After follow bug number at bug tracking system between // signs.
8
+ And then follow a short description of the bug.
9
+
10
+ Example:
11
+ For example bug: "1.0 { Feature Request } [ AntHill ] / 380 / STLport support required" means
12
+ that bug was created for 1.0 version of the AntHill component, bug is feature request with
13
+ 380 number at bug tracking system. And bug requires STLPort support implementation.
14
+
15
+ Version 0.2
16
+ -----------
17
+ 0.2 { Feature Request } [ Clockworkd ] / X / Add tests
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Clockworkd::Command do
4
+ it "should define class Command" do
5
+ ::Clockworkd::Command.should be
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Clockworkd::CronLine do
4
+ it "should define class CronLine" do
5
+ ::Clockworkd::CronLine.should be
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Clockworkd::Event do
4
+ it "should define class Event" do
5
+ ::Clockworkd::Event.should be
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Clockworkd::Worker do
4
+ it "should define class Worker" do
5
+ ::Clockworkd::Worker.should be
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe Clockworkd do
4
+ it "should define rails" do
5
+ ::Rails::VERSION::MAJOR.should be
6
+ end
7
+
8
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --backtrace
@@ -0,0 +1,26 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+ $:.unshift File.join(File.dirname(__FILE__), '../lib')
3
+
4
+ ENV["RAILS_ENV"] = "test"
5
+ require 'rubygems'
6
+ require 'bundler/setup'
7
+ require 'rspec'
8
+ require 'logger'
9
+ require 'rails'
10
+ require 'action_controller'
11
+ require 'clockworkd'
12
+
13
+ module Rails
14
+ module VERSION
15
+ MAJOR = 3
16
+ end
17
+ end unless defined? Rails
18
+
19
+ # Clockworkd.root = './'
20
+ RAILS_ROOT = './' unless defined?(RAILS_ROOT)
21
+ RAILS_ENV = 'test' unless defined?(RAILS_ENV)
22
+
23
+ RSpec.configure do |config|
24
+ config.mock_with :rspec
25
+ end
26
+
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: clockworkd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.5
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Adam Wiggins
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-07-31 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: tzinfo
16
+ requirement: &30906132 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.3.23
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *30906132
25
+ description: A scheduler process to replace cron, using a more flexible Ruby syntax
26
+ running as a single long-running process. Inspired by rufus-scheduler and resque-scheduler.
27
+ email: adam@heroku.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files:
31
+ - README
32
+ files:
33
+ - changelog
34
+ - clockworkd.gemspec
35
+ - contrib/clockworkd.monitrc
36
+ - generators/clockworkd/clockworkd_generator.rb
37
+ - generators/clockworkd/templates/clockworkd.yml
38
+ - generators/clockworkd/templates/script
39
+ - knownbugs
40
+ - lib/clockworkd/command.rb
41
+ - lib/clockworkd/cronline.rb
42
+ - lib/clockworkd/event.rb
43
+ - lib/clockworkd/recipes.rb
44
+ - lib/clockworkd/worker.rb
45
+ - lib/clockworkd.rb
46
+ - lib/generators/clockworkd/clockworkd_generator.rb
47
+ - lib/generators/clockworkd/templates/clockworkd.yml
48
+ - lib/generators/clockworkd/templates/script
49
+ - MIT-LICENSE
50
+ - Rakefile
51
+ - README
52
+ - recipes/clockworkd.rb
53
+ - roadmap
54
+ - spec/lib/clockworkd/command_spec.rb
55
+ - spec/lib/clockworkd/cronline_spec.rb
56
+ - spec/lib/clockworkd/event_spec.rb
57
+ - spec/lib/clockworkd/worker_spec.rb
58
+ - spec/lib/clockworkd_spec.rb
59
+ - spec/spec.opts
60
+ - spec/spec_helper.rb
61
+ - VERSION
62
+ homepage: http://www.majoron.com/project/rbundle/clockworkd
63
+ licenses: []
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project: clockworkd
82
+ rubygems_version: 1.8.10
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: A scheduler process to replace cron.
86
+ test_files:
87
+ - spec/lib/clockworkd/command_spec.rb
88
+ - spec/lib/clockworkd/cronline_spec.rb
89
+ - spec/lib/clockworkd/event_spec.rb
90
+ - spec/lib/clockworkd/worker_spec.rb
91
+ - spec/lib/clockworkd_spec.rb
92
+ - spec/spec.opts
93
+ - spec/spec_helper.rb