clockworkd 0.2.5

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/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