rufus-scheduler 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/CHANGELOG.txt +6 -0
- data/README.txt +56 -0
- data/lib/openwfe/util/scheduler.rb +41 -0
- data/lib/rufus/otime.rb +242 -0
- data/lib/rufus/scheduler.rb +1335 -0
- data/test/cron_test.rb +134 -0
- data/test/cronline_test.rb +55 -0
- data/test/scheduler_0_test.rb +350 -0
- data/test/scheduler_1_test.rb +93 -0
- data/test/scheduler_2_test.rb +123 -0
- data/test/scheduler_3_test.rb +69 -0
- data/test/scheduler_4_test.rb +81 -0
- data/test/test.rb +10 -0
- metadata +69 -0
data/CHANGELOG.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
|
2
|
+
= rufus-scheduler
|
3
|
+
|
4
|
+
This gem was formerly known as 'openwferu-scheduler'. It has been repackaged as 'rufus-scheduler'. Old 'require' paths have been kept for backward compatibility (no need to update your code).
|
5
|
+
|
6
|
+
The new license is MIT (not much of a change, the previous license was BSD).
|
7
|
+
|
8
|
+
|
9
|
+
== getting it
|
10
|
+
|
11
|
+
sudo gem install rufus-scheduler
|
12
|
+
|
13
|
+
or at
|
14
|
+
|
15
|
+
http://rubyforge.org/frs/?group_id=4812
|
16
|
+
|
17
|
+
|
18
|
+
== usage
|
19
|
+
|
20
|
+
See the Rufus::Scheduler class rdoc itself or the original OpenWFEru scheduler documentation at http://openwferu.rubyforge.org/scheduler.html
|
21
|
+
|
22
|
+
|
23
|
+
== dependencies
|
24
|
+
|
25
|
+
None.
|
26
|
+
|
27
|
+
|
28
|
+
== mailing list
|
29
|
+
|
30
|
+
On the rufus-ruby list[http://groups.google.com/group/rufus-ruby] :
|
31
|
+
|
32
|
+
http://groups.google.com/group/rufus-ruby
|
33
|
+
|
34
|
+
|
35
|
+
== issue tracker
|
36
|
+
|
37
|
+
http://rubyforge.org/tracker/?atid=18584&group_id=4812&func=browse
|
38
|
+
|
39
|
+
|
40
|
+
== source
|
41
|
+
|
42
|
+
http://rufus.rubyforge.org/svn/trunk/scheduler
|
43
|
+
|
44
|
+
svn checkout http://rufus.rubyforge.org/svn/trunk/scheduler
|
45
|
+
|
46
|
+
|
47
|
+
== author
|
48
|
+
|
49
|
+
John Mettraux, jmettraux@gmail.com
|
50
|
+
http://jmettraux.wordpress.com
|
51
|
+
|
52
|
+
|
53
|
+
== license
|
54
|
+
|
55
|
+
MIT
|
56
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
#
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2006-2008, John Mettraux, jmettraux@gmail.com
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'rufus/scheduler'
|
26
|
+
|
27
|
+
|
28
|
+
#
|
29
|
+
# An 'alias' to Rufus::Scheduler to keep backward compatibility for
|
30
|
+
# the users of the gem 'openwferu-scheduler'.
|
31
|
+
#
|
32
|
+
module OpenWFE
|
33
|
+
|
34
|
+
class Scheduler < Rufus::Scheduler
|
35
|
+
end
|
36
|
+
|
37
|
+
module Schedulable
|
38
|
+
include Rufus::Schedulable
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
data/lib/rufus/otime.rb
ADDED
@@ -0,0 +1,242 @@
|
|
1
|
+
#
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2005-2008, John Mettraux, jmettraux@gmail.com
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
#
|
24
|
+
|
25
|
+
#
|
26
|
+
# "hecho en Costa Rica"
|
27
|
+
#
|
28
|
+
# john.mettraux@openwfe.org
|
29
|
+
#
|
30
|
+
|
31
|
+
require 'date'
|
32
|
+
#require 'parsedate'
|
33
|
+
|
34
|
+
|
35
|
+
module Rufus
|
36
|
+
|
37
|
+
#TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
38
|
+
|
39
|
+
#
|
40
|
+
# Returns the current time as an ISO date string
|
41
|
+
#
|
42
|
+
def Rufus.now
|
43
|
+
|
44
|
+
to_iso8601_date(Time.new())
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# As the name implies.
|
49
|
+
#
|
50
|
+
def Rufus.to_iso8601_date (date)
|
51
|
+
|
52
|
+
if date.kind_of? Float
|
53
|
+
date = to_datetime(Time.at(date))
|
54
|
+
elsif date.kind_of? Time
|
55
|
+
date = to_datetime(date)
|
56
|
+
elsif not date.kind_of? Date
|
57
|
+
date = DateTime.parse(date)
|
58
|
+
end
|
59
|
+
|
60
|
+
s = date.to_s # this is costly
|
61
|
+
s[10] = " "
|
62
|
+
|
63
|
+
s
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# the old method we used to generate our ISO datetime strings
|
68
|
+
#
|
69
|
+
def Rufus.time_to_iso8601_date (time)
|
70
|
+
|
71
|
+
s = time.getutc().strftime(TIME_FORMAT)
|
72
|
+
o = time.utc_offset / 3600
|
73
|
+
o = o.to_s + "00"
|
74
|
+
o = "0" + o if o.length < 4
|
75
|
+
o = "+" + o unless o[0..1] == '-'
|
76
|
+
|
77
|
+
s + " " + o.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Returns a Ruby time
|
82
|
+
#
|
83
|
+
def Rufus.to_ruby_time (iso_date)
|
84
|
+
|
85
|
+
DateTime.parse(iso_date)
|
86
|
+
end
|
87
|
+
|
88
|
+
#def Rufus.parse_date (date)
|
89
|
+
#end
|
90
|
+
|
91
|
+
#
|
92
|
+
# equivalent to java.lang.System.currentTimeMillis()
|
93
|
+
#
|
94
|
+
def Rufus.current_time_millis
|
95
|
+
|
96
|
+
(Time.new.to_f * 1000).to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
#
|
100
|
+
# turns a string like '1m10s' into a float like '70.0'
|
101
|
+
#
|
102
|
+
# w -> week
|
103
|
+
# d -> day
|
104
|
+
# h -> hour
|
105
|
+
# m -> minute
|
106
|
+
# s -> second
|
107
|
+
# M -> month
|
108
|
+
# y -> year
|
109
|
+
# 'nada' -> millisecond
|
110
|
+
#
|
111
|
+
def Rufus.parse_time_string (string)
|
112
|
+
|
113
|
+
string = string.strip
|
114
|
+
|
115
|
+
index = -1
|
116
|
+
result = 0.0
|
117
|
+
|
118
|
+
number = ""
|
119
|
+
|
120
|
+
loop do
|
121
|
+
|
122
|
+
index = index + 1
|
123
|
+
|
124
|
+
if index >= string.length
|
125
|
+
if number.length > 0
|
126
|
+
result = result + (Float(number) / 1000.0)
|
127
|
+
end
|
128
|
+
break
|
129
|
+
end
|
130
|
+
|
131
|
+
c = string[index, 1]
|
132
|
+
|
133
|
+
# TODO : investigate something better than this is_digit?
|
134
|
+
|
135
|
+
if is_digit?(c)
|
136
|
+
number = number + c
|
137
|
+
next
|
138
|
+
end
|
139
|
+
|
140
|
+
value = Integer(number)
|
141
|
+
number = ""
|
142
|
+
|
143
|
+
multiplier = DURATIONS[c]
|
144
|
+
|
145
|
+
raise "unknown time char '#{c}'" \
|
146
|
+
if not multiplier
|
147
|
+
|
148
|
+
result = result + (value * multiplier)
|
149
|
+
end
|
150
|
+
|
151
|
+
result
|
152
|
+
end
|
153
|
+
|
154
|
+
#
|
155
|
+
# returns true if the character c is a digit
|
156
|
+
#
|
157
|
+
def Rufus.is_digit? (c)
|
158
|
+
|
159
|
+
return false if not c.kind_of?(String)
|
160
|
+
return false if c.length > 1
|
161
|
+
(c >= "0" and c <= "9")
|
162
|
+
end
|
163
|
+
|
164
|
+
#
|
165
|
+
# conversion methods between Date[Time] and Time
|
166
|
+
|
167
|
+
#
|
168
|
+
# Ruby Cookbook 1st edition p.111
|
169
|
+
# http://www.oreilly.com/catalog/rubyckbk/
|
170
|
+
# a must
|
171
|
+
#
|
172
|
+
|
173
|
+
#
|
174
|
+
# converts a Time instance to a DateTime one
|
175
|
+
#
|
176
|
+
def Rufus.to_datetime (time)
|
177
|
+
|
178
|
+
s = time.sec + Rational(time.usec, 10**6)
|
179
|
+
o = Rational(time.utc_offset, 3600 * 24)
|
180
|
+
|
181
|
+
begin
|
182
|
+
|
183
|
+
DateTime.new(
|
184
|
+
time.year,
|
185
|
+
time.month,
|
186
|
+
time.day,
|
187
|
+
time.hour,
|
188
|
+
time.min,
|
189
|
+
s,
|
190
|
+
o)
|
191
|
+
|
192
|
+
rescue Exception => e
|
193
|
+
|
194
|
+
#puts
|
195
|
+
#puts OpenWFE::exception_to_s(e)
|
196
|
+
#puts
|
197
|
+
#puts \
|
198
|
+
# "\n Date.new() problem. Params :"+
|
199
|
+
# "\n....y:#{time.year} M:#{time.month} d:#{time.day} "+
|
200
|
+
# "h:#{time.hour} m:#{time.min} s:#{s} o:#{o}"
|
201
|
+
|
202
|
+
DateTime.new(
|
203
|
+
time.year,
|
204
|
+
time.month,
|
205
|
+
time.day,
|
206
|
+
time.hour,
|
207
|
+
time.min,
|
208
|
+
time.sec,
|
209
|
+
time.utc_offset)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def Rufus.to_gm_time (dtime)
|
214
|
+
|
215
|
+
to_ttime(dtime.new_offset, :gm)
|
216
|
+
end
|
217
|
+
|
218
|
+
def Rufus.to_local_time (dtime)
|
219
|
+
|
220
|
+
to_ttime(dtime.new_offset(DateTime.now.offset-offset), :local)
|
221
|
+
end
|
222
|
+
|
223
|
+
def Rufus.to_ttime (d, method)
|
224
|
+
|
225
|
+
usec = (d.sec_fraction * 3600 * 24 * (10**6)).to_i
|
226
|
+
Time.send(method, d.year, d.month, d.day, d.hour, d.min, d.sec, usec)
|
227
|
+
end
|
228
|
+
|
229
|
+
protected
|
230
|
+
|
231
|
+
DURATIONS = {
|
232
|
+
"y" => 365 * 24 * 3600,
|
233
|
+
"M" => 30 * 24 * 3600,
|
234
|
+
"w" => 7 * 24 * 3600,
|
235
|
+
"d" => 24 * 3600,
|
236
|
+
"h" => 3600,
|
237
|
+
"m" => 60,
|
238
|
+
"s" => 1
|
239
|
+
}
|
240
|
+
|
241
|
+
end
|
242
|
+
|
@@ -0,0 +1,1335 @@
|
|
1
|
+
#
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2006-2008, John Mettraux, jmettraux@gmail.com
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
13
|
+
# all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
# THE SOFTWARE.
|
22
|
+
#++
|
23
|
+
#
|
24
|
+
|
25
|
+
#
|
26
|
+
# "made in Japan"
|
27
|
+
#
|
28
|
+
# John Mettraux at openwfe.org
|
29
|
+
#
|
30
|
+
|
31
|
+
require 'thread'
|
32
|
+
require 'monitor'
|
33
|
+
require 'rufus/otime'
|
34
|
+
|
35
|
+
|
36
|
+
module Rufus
|
37
|
+
|
38
|
+
#
|
39
|
+
# The Scheduler is used by OpenWFEru for registering 'at' and 'cron' jobs.
|
40
|
+
# 'at' jobs to execute once at a given point in time. 'cron' jobs
|
41
|
+
# execute a specified intervals.
|
42
|
+
# The two main methods are thus schedule_at() and schedule().
|
43
|
+
#
|
44
|
+
# schedule_at() and schedule() await either a Schedulable instance and
|
45
|
+
# params (usually an array or nil), either a block, which is more in the
|
46
|
+
# Ruby way.
|
47
|
+
#
|
48
|
+
# == Examples
|
49
|
+
#
|
50
|
+
# scheduler.schedule_in("3d") do
|
51
|
+
# regenerate_monthly_report()
|
52
|
+
# end
|
53
|
+
# #
|
54
|
+
# # will call the regenerate_monthly_report method
|
55
|
+
# # in 3 days from now
|
56
|
+
#
|
57
|
+
# scheduler.schedule "0 22 * * 1-5" do
|
58
|
+
# log.info "activating security system..."
|
59
|
+
# activate_security_system()
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# job_id = scheduler.schedule_at "Sun Oct 07 14:24:01 +0900 2009" do
|
63
|
+
# init_self_destruction_sequence()
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# an example that uses a Schedulable class :
|
67
|
+
#
|
68
|
+
# class Regenerator < Schedulable
|
69
|
+
# def trigger (frequency)
|
70
|
+
# self.send(frequency)
|
71
|
+
# end
|
72
|
+
# def monthly
|
73
|
+
# # ...
|
74
|
+
# end
|
75
|
+
# def yearly
|
76
|
+
# # ...
|
77
|
+
# end
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# regenerator = Regenerator.new
|
81
|
+
#
|
82
|
+
# scheduler.schedule_in("4d", regenerator)
|
83
|
+
# #
|
84
|
+
# # will regenerate the report in four days
|
85
|
+
#
|
86
|
+
# scheduler.schedule_in(
|
87
|
+
# "5d",
|
88
|
+
# { :schedulable => regenerator, :scope => :month })
|
89
|
+
# #
|
90
|
+
# # will regenerate the monthly report in 5 days
|
91
|
+
#
|
92
|
+
# There is also schedule_every() :
|
93
|
+
#
|
94
|
+
# scheduler.schedule_every("1h20m") do
|
95
|
+
# regenerate_latest_report()
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# The scheduler has a "exit_when_no_more_jobs" attribute. When set to
|
99
|
+
# 'true', the scheduler will exit as soon as there are no more jobs to
|
100
|
+
# run.
|
101
|
+
# Use with care though, if you create a scheduler, set this attribute
|
102
|
+
# to true and start the scheduler, the scheduler will immediately exit.
|
103
|
+
# This attribute is best used indirectly : the method
|
104
|
+
# join_until_no_more_jobs() wraps it.
|
105
|
+
#
|
106
|
+
# The :scheduler_precision can be set when instantiating the scheduler.
|
107
|
+
#
|
108
|
+
# scheduler = Rufus::Scheduler.new(:scheduler_precision => 0.500)
|
109
|
+
# scheduler.start
|
110
|
+
# #
|
111
|
+
# # instatiates a scheduler that checks its jobs twice per second
|
112
|
+
# # (the default is 4 times per second (0.250))
|
113
|
+
#
|
114
|
+
#
|
115
|
+
# == Tags
|
116
|
+
#
|
117
|
+
# Tags can be attached to jobs scheduled :
|
118
|
+
#
|
119
|
+
# scheduler.schedule_in "2h", :tags => "backup" do
|
120
|
+
# init_backup_sequence()
|
121
|
+
# end
|
122
|
+
#
|
123
|
+
# scheduler.schedule "0 24 * * *", :tags => "new_day" do
|
124
|
+
# do_this_or_that()
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# jobs = find_jobs 'backup'
|
128
|
+
# jobs.each { |job| job.unschedule }
|
129
|
+
#
|
130
|
+
# Multiple tags may be attached to a single job :
|
131
|
+
#
|
132
|
+
# scheduler.schedule_in "2h", :tags => [ "backup", "important" ] do
|
133
|
+
# init_backup_sequence()
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
# The vanilla case for tags assume they are String instances, but nothing
|
137
|
+
# prevents you from using anything else. The scheduler has no persistence
|
138
|
+
# by itself, so no serialization issue.
|
139
|
+
#
|
140
|
+
#
|
141
|
+
# == Cron up to the second
|
142
|
+
#
|
143
|
+
# A cron schedule can be set at the second level :
|
144
|
+
#
|
145
|
+
# scheduler.schedule "7 * * * * *" do
|
146
|
+
# puts "it's now the seventh second of the minute"
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# The rufus scheduler recognizes an optional first column for second
|
150
|
+
# scheduling. This column can, like for the other columns, specify a
|
151
|
+
# value ("7"), a list of values ("7,8,9,27") or a range ("7-12").
|
152
|
+
#
|
153
|
+
# == Exceptions
|
154
|
+
#
|
155
|
+
# The rufus scheduler will output a stacktrace to the STDOUT in
|
156
|
+
# case of exception. There are two ways to change that behaviour.
|
157
|
+
#
|
158
|
+
# # 1 - providing a lwarn method to the scheduler instance :
|
159
|
+
#
|
160
|
+
# class << scheduler
|
161
|
+
# def lwarn (&block)
|
162
|
+
# puts "oops, something wrong happened : "
|
163
|
+
# puts block.call
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# # 2 - overriding the [protected] method log_exception(e) :
|
168
|
+
#
|
169
|
+
# class << scheduler
|
170
|
+
# def log_exception (e)
|
171
|
+
# puts "something wrong happened : "+e.to_s
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
# == 'Every jobs' and rescheduling
|
176
|
+
#
|
177
|
+
# Every jobs can reschedule/unschedule themselves. A reschedule example :
|
178
|
+
#
|
179
|
+
# schedule.schedule_every "5h" do |job_id, at, params|
|
180
|
+
#
|
181
|
+
# mails = $inbox.fetch_mails
|
182
|
+
# mails.each { |m| $inbox.mark_as_spam(m) if is_spam(m) }
|
183
|
+
#
|
184
|
+
# params[:every] = if mails.size > 100
|
185
|
+
# "1h" # lots of spam, check every hour
|
186
|
+
# else
|
187
|
+
# "5h" # normal schedule, every 5 hours
|
188
|
+
# end
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# Unschedule example :
|
192
|
+
#
|
193
|
+
# schedule.schedule_every "10s" do |job_id, at, params|
|
194
|
+
# #
|
195
|
+
# # polls every 10 seconds until a mail arrives
|
196
|
+
#
|
197
|
+
# $mail = $inbox.fetch_last_mail
|
198
|
+
#
|
199
|
+
# params[:dont_reschedule] = true if $mail
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
class Scheduler
|
203
|
+
|
204
|
+
#
|
205
|
+
# By default, the precision is 0.250, with means the scheduler
|
206
|
+
# will check for jobs to execute 4 times per second.
|
207
|
+
#
|
208
|
+
attr_accessor :precision
|
209
|
+
|
210
|
+
#
|
211
|
+
# As its name implies.
|
212
|
+
#
|
213
|
+
attr_accessor :stopped
|
214
|
+
|
215
|
+
|
216
|
+
def initialize (params={})
|
217
|
+
|
218
|
+
super()
|
219
|
+
|
220
|
+
@pending_jobs = []
|
221
|
+
@cron_jobs = {}
|
222
|
+
|
223
|
+
@schedule_queue = Queue.new
|
224
|
+
@unschedule_queue = Queue.new
|
225
|
+
#
|
226
|
+
# sync between the step() method and the [un]schedule
|
227
|
+
# methods is done via these queues, no more mutex
|
228
|
+
|
229
|
+
@scheduler_thread = nil
|
230
|
+
|
231
|
+
@precision = 0.250
|
232
|
+
# every 250ms, the scheduler wakes up (default value)
|
233
|
+
begin
|
234
|
+
@precision = Float(params[:scheduler_precision])
|
235
|
+
rescue Exception => e
|
236
|
+
# let precision at its default value
|
237
|
+
end
|
238
|
+
|
239
|
+
@exit_when_no_more_jobs = false
|
240
|
+
@dont_reschedule_every = false
|
241
|
+
|
242
|
+
@last_cron_second = -1
|
243
|
+
|
244
|
+
@stopped = true
|
245
|
+
end
|
246
|
+
|
247
|
+
#
|
248
|
+
# Starts this scheduler (or restart it if it was previously stopped)
|
249
|
+
#
|
250
|
+
def sstart
|
251
|
+
|
252
|
+
@stopped = false
|
253
|
+
|
254
|
+
@scheduler_thread = Thread.new do
|
255
|
+
|
256
|
+
if defined?(JRUBY_VERSION)
|
257
|
+
|
258
|
+
require 'java'
|
259
|
+
|
260
|
+
java.lang.Thread.current_thread.name = \
|
261
|
+
"openwferu scheduler (Ruby Thread)"
|
262
|
+
end
|
263
|
+
|
264
|
+
loop do
|
265
|
+
|
266
|
+
break if @stopped
|
267
|
+
|
268
|
+
step
|
269
|
+
|
270
|
+
sleep @precision
|
271
|
+
# TODO : adjust precision
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
#
|
277
|
+
# The scheduler is stoppable via sstop()
|
278
|
+
#
|
279
|
+
def sstop
|
280
|
+
|
281
|
+
@stopped = true
|
282
|
+
end
|
283
|
+
|
284
|
+
alias :start :sstart
|
285
|
+
alias :stop :sstop
|
286
|
+
|
287
|
+
#
|
288
|
+
# Joins on the scheduler thread
|
289
|
+
#
|
290
|
+
def join
|
291
|
+
|
292
|
+
@scheduler_thread.join
|
293
|
+
end
|
294
|
+
|
295
|
+
#
|
296
|
+
# Like join() but takes care of setting the 'exit_when_no_more_jobs'
|
297
|
+
# attribute of this scheduler to true before joining.
|
298
|
+
# Thus the scheduler will exit (and the join terminates) as soon as
|
299
|
+
# there aren't no more 'at' (or 'every') jobs in the scheduler.
|
300
|
+
#
|
301
|
+
# Currently used only in unit tests.
|
302
|
+
#
|
303
|
+
def join_until_no_more_jobs
|
304
|
+
|
305
|
+
@exit_when_no_more_jobs = true
|
306
|
+
join
|
307
|
+
end
|
308
|
+
|
309
|
+
#--
|
310
|
+
#
|
311
|
+
# The scheduling methods
|
312
|
+
#
|
313
|
+
#++
|
314
|
+
|
315
|
+
#
|
316
|
+
# Schedules a job by specifying at which time it should trigger.
|
317
|
+
# Returns the a job_id that can be used to unschedule the job.
|
318
|
+
#
|
319
|
+
# This method returns a job identifier which can be used to unschedule()
|
320
|
+
# the job.
|
321
|
+
#
|
322
|
+
# If the job is specified in the past, it will be triggered immediately
|
323
|
+
# but not scheduled.
|
324
|
+
# To avoid the triggering, the parameter :discard_past may be set to
|
325
|
+
# true :
|
326
|
+
#
|
327
|
+
# jobid = scheduler.schedule_at(yesterday, :discard_past => true) do
|
328
|
+
# puts "you'll never read this message"
|
329
|
+
# end
|
330
|
+
#
|
331
|
+
# And 'jobid' will hold a nil (not scheduled).
|
332
|
+
#
|
333
|
+
#
|
334
|
+
def schedule_at (at, params={}, &block)
|
335
|
+
|
336
|
+
do_schedule_at(
|
337
|
+
at,
|
338
|
+
prepare_params(params),
|
339
|
+
&block)
|
340
|
+
end
|
341
|
+
|
342
|
+
|
343
|
+
#
|
344
|
+
# Schedules a job by stating in how much time it should trigger.
|
345
|
+
# Returns the a job_id that can be used to unschedule the job.
|
346
|
+
#
|
347
|
+
# This method returns a job identifier which can be used to unschedule()
|
348
|
+
# the job.
|
349
|
+
#
|
350
|
+
def schedule_in (duration, params={}, &block)
|
351
|
+
|
352
|
+
do_schedule_at(
|
353
|
+
Time.new.to_f + duration_to_f(duration),
|
354
|
+
prepare_params(params),
|
355
|
+
&block)
|
356
|
+
end
|
357
|
+
|
358
|
+
#
|
359
|
+
# Schedules a job in a loop. After an execution, it will not execute
|
360
|
+
# before the time specified in 'freq'.
|
361
|
+
#
|
362
|
+
# This method returns a job identifier which can be used to unschedule()
|
363
|
+
# the job.
|
364
|
+
#
|
365
|
+
# In case of exception in the job, it will be rescheduled. If you don't
|
366
|
+
# want the job to be rescheduled, set the parameter :try_again to false.
|
367
|
+
#
|
368
|
+
# scheduler.schedule_every "500", :try_again => false do
|
369
|
+
# do_some_prone_to_error_stuff()
|
370
|
+
# # won't get rescheduled in case of exception
|
371
|
+
# end
|
372
|
+
#
|
373
|
+
def schedule_every (freq, params={}, &block)
|
374
|
+
|
375
|
+
f = duration_to_f freq
|
376
|
+
|
377
|
+
params = prepare_params params
|
378
|
+
schedulable = params[:schedulable]
|
379
|
+
params[:every] = freq
|
380
|
+
|
381
|
+
last_at = params[:last_at]
|
382
|
+
next_at = if last_at
|
383
|
+
last_at + f
|
384
|
+
else
|
385
|
+
Time.now.to_f + f
|
386
|
+
end
|
387
|
+
|
388
|
+
do_schedule_at(next_at, params) do |job_id, at|
|
389
|
+
|
390
|
+
#
|
391
|
+
# trigger ...
|
392
|
+
|
393
|
+
hit_exception = false
|
394
|
+
|
395
|
+
begin
|
396
|
+
|
397
|
+
if schedulable
|
398
|
+
schedulable.trigger params
|
399
|
+
else
|
400
|
+
block.call job_id, at, params
|
401
|
+
end
|
402
|
+
|
403
|
+
rescue Exception => e
|
404
|
+
|
405
|
+
log_exception e
|
406
|
+
|
407
|
+
hit_exception = true
|
408
|
+
end
|
409
|
+
|
410
|
+
# cannot use a return here !!! (block)
|
411
|
+
|
412
|
+
unless \
|
413
|
+
@dont_reschedule_every or
|
414
|
+
(params[:dont_reschedule] == true) or
|
415
|
+
(hit_exception and params[:try_again] == false)
|
416
|
+
|
417
|
+
#
|
418
|
+
# ok, reschedule ...
|
419
|
+
|
420
|
+
params[:job_id] = job_id
|
421
|
+
params[:last_at] = at
|
422
|
+
|
423
|
+
schedule_every params[:every], params, &block
|
424
|
+
#
|
425
|
+
# yes, this is a kind of recursion
|
426
|
+
|
427
|
+
# note that params[:every] might have been changed
|
428
|
+
# by the block/schedulable code
|
429
|
+
end
|
430
|
+
|
431
|
+
job_id
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
#
|
436
|
+
# Schedules a cron job, the 'cron_line' is a string
|
437
|
+
# following the Unix cron standard (see "man 5 crontab" in your command
|
438
|
+
# line, or http://www.google.com/search?q=man%205%20crontab).
|
439
|
+
#
|
440
|
+
# For example :
|
441
|
+
#
|
442
|
+
# scheduler.schedule("5 0 * * *", s)
|
443
|
+
# # will trigger the schedulable s every day
|
444
|
+
# # five minutes after midnight
|
445
|
+
#
|
446
|
+
# scheduler.schedule("15 14 1 * *", s)
|
447
|
+
# # will trigger s at 14:15 on the first of every month
|
448
|
+
#
|
449
|
+
# scheduler.schedule("0 22 * * 1-5") do
|
450
|
+
# puts "it's break time..."
|
451
|
+
# end
|
452
|
+
# # outputs a message every weekday at 10pm
|
453
|
+
#
|
454
|
+
# Returns the job id attributed to this 'cron job', this id can
|
455
|
+
# be used to unschedule the job.
|
456
|
+
#
|
457
|
+
# This method returns a job identifier which can be used to unschedule()
|
458
|
+
# the job.
|
459
|
+
#
|
460
|
+
def schedule (cron_line, params={}, &block)
|
461
|
+
|
462
|
+
params = prepare_params(params)
|
463
|
+
|
464
|
+
#
|
465
|
+
# is a job with the same id already scheduled ?
|
466
|
+
|
467
|
+
cron_id = params[:cron_id]
|
468
|
+
cron_id = params[:job_id] unless cron_id
|
469
|
+
|
470
|
+
#unschedule(cron_id) if cron_id
|
471
|
+
@unschedule_queue << [ :cron, cron_id ]
|
472
|
+
|
473
|
+
#
|
474
|
+
# schedule
|
475
|
+
|
476
|
+
b = to_block(params, &block)
|
477
|
+
job = CronJob.new(self, cron_id, cron_line, params, &b)
|
478
|
+
|
479
|
+
#@cron_jobs[job.job_id] = job
|
480
|
+
@schedule_queue << job
|
481
|
+
|
482
|
+
job.job_id
|
483
|
+
end
|
484
|
+
|
485
|
+
#--
|
486
|
+
#
|
487
|
+
# The UNscheduling methods
|
488
|
+
#
|
489
|
+
#++
|
490
|
+
|
491
|
+
#
|
492
|
+
# Unschedules an 'at' or a 'cron' job identified by the id
|
493
|
+
# it was given at schedule time.
|
494
|
+
#
|
495
|
+
def unschedule (job_id)
|
496
|
+
|
497
|
+
@unschedule_queue << [ :at, job_id ]
|
498
|
+
end
|
499
|
+
|
500
|
+
#
|
501
|
+
# Unschedules a cron job
|
502
|
+
#
|
503
|
+
def unschedule_cron_job (job_id)
|
504
|
+
|
505
|
+
@unschedule_queue << [ :cron, job_id ]
|
506
|
+
end
|
507
|
+
|
508
|
+
#--
|
509
|
+
#
|
510
|
+
# 'query' methods
|
511
|
+
#
|
512
|
+
#++
|
513
|
+
|
514
|
+
#
|
515
|
+
# Returns the job corresponding to job_id, an instance of AtJob
|
516
|
+
# or CronJob will be returned.
|
517
|
+
#
|
518
|
+
def get_job (job_id)
|
519
|
+
|
520
|
+
job = @cron_jobs[job_id]
|
521
|
+
return job if job
|
522
|
+
|
523
|
+
@pending_jobs.find do |job|
|
524
|
+
job.job_id == job_id
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
#
|
529
|
+
# Finds a job (via get_job()) and then returns the wrapped
|
530
|
+
# schedulable if any.
|
531
|
+
#
|
532
|
+
def get_schedulable (job_id)
|
533
|
+
|
534
|
+
#return nil unless job_id
|
535
|
+
|
536
|
+
j = get_job(job_id)
|
537
|
+
|
538
|
+
return j.schedulable if j.respond_to?(:schedulable)
|
539
|
+
|
540
|
+
nil
|
541
|
+
end
|
542
|
+
|
543
|
+
#
|
544
|
+
# Returns an array of jobs that have the given tag.
|
545
|
+
#
|
546
|
+
def find_jobs (tag)
|
547
|
+
|
548
|
+
result = @cron_jobs.values.find_all do |job|
|
549
|
+
job.has_tag?(tag)
|
550
|
+
end
|
551
|
+
|
552
|
+
result + @pending_jobs.find_all do |job|
|
553
|
+
job.has_tag?(tag)
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
#
|
558
|
+
# Finds the jobs with the given tag and then returns an array of
|
559
|
+
# the wrapped Schedulable objects.
|
560
|
+
# Jobs that haven't a wrapped Schedulable won't be included in the
|
561
|
+
# result.
|
562
|
+
#
|
563
|
+
def find_schedulables (tag)
|
564
|
+
|
565
|
+
#jobs = find_jobs(tag)
|
566
|
+
#result = []
|
567
|
+
#jobs.each do |job|
|
568
|
+
# result.push(job.schedulable) if job.respond_to?(:schedulable)
|
569
|
+
#end
|
570
|
+
#result
|
571
|
+
|
572
|
+
find_jobs(tags).inject([]) do |result, job|
|
573
|
+
|
574
|
+
result.push(job.schedulable) if job.respond_to?(:schedulable)
|
575
|
+
result
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
#
|
580
|
+
# Returns the number of currently pending jobs in this scheduler
|
581
|
+
# ('at' jobs and 'every' jobs).
|
582
|
+
#
|
583
|
+
def pending_job_count
|
584
|
+
|
585
|
+
@pending_jobs.size
|
586
|
+
end
|
587
|
+
|
588
|
+
#
|
589
|
+
# Returns the number of cron jobs currently active in this scheduler.
|
590
|
+
#
|
591
|
+
def cron_job_count
|
592
|
+
|
593
|
+
@cron_jobs.size
|
594
|
+
end
|
595
|
+
|
596
|
+
#
|
597
|
+
# Returns the current count of 'every' jobs scheduled.
|
598
|
+
#
|
599
|
+
def every_job_count
|
600
|
+
|
601
|
+
@pending_jobs.select { |j| j.is_a?(EveryJob) }.size
|
602
|
+
end
|
603
|
+
|
604
|
+
#
|
605
|
+
# Returns the current count of 'at' jobs scheduled (not 'every').
|
606
|
+
#
|
607
|
+
def at_job_count
|
608
|
+
|
609
|
+
@pending_jobs.select { |j| j.instance_of?(AtJob) }.size
|
610
|
+
end
|
611
|
+
|
612
|
+
#
|
613
|
+
# Returns true if the given string seems to be a cron string.
|
614
|
+
#
|
615
|
+
def Scheduler.is_cron_string (s)
|
616
|
+
|
617
|
+
s.match(".+ .+ .+ .+ .+")
|
618
|
+
end
|
619
|
+
|
620
|
+
#protected
|
621
|
+
private
|
622
|
+
|
623
|
+
def do_unschedule (job_id)
|
624
|
+
|
625
|
+
for i in 0...@pending_jobs.length
|
626
|
+
if @pending_jobs[i].job_id == job_id
|
627
|
+
@pending_jobs.delete_at i
|
628
|
+
return true
|
629
|
+
end
|
630
|
+
end
|
631
|
+
#
|
632
|
+
# not using delete_if because it scans the whole list
|
633
|
+
|
634
|
+
do_unschedule_cron_job job_id
|
635
|
+
end
|
636
|
+
|
637
|
+
def do_unschedule_cron_job (job_id)
|
638
|
+
|
639
|
+
(@cron_jobs.delete(job_id) != nil)
|
640
|
+
end
|
641
|
+
|
642
|
+
#
|
643
|
+
# Making sure that params is a Hash.
|
644
|
+
#
|
645
|
+
def prepare_params (params)
|
646
|
+
|
647
|
+
params = { :schedulable => params } \
|
648
|
+
if params.is_a?(Schedulable)
|
649
|
+
params
|
650
|
+
end
|
651
|
+
|
652
|
+
#
|
653
|
+
# The core method behind schedule_at and schedule_in (and also
|
654
|
+
# schedule_every). It's protected, don't use it directly.
|
655
|
+
#
|
656
|
+
def do_schedule_at (at, params={}, &block)
|
657
|
+
|
658
|
+
#puts "0 at is '#{at.to_s}' (#{at.class})"
|
659
|
+
|
660
|
+
at = Rufus::to_ruby_time(at) \
|
661
|
+
if at.kind_of?(String)
|
662
|
+
|
663
|
+
at = Rufus::to_gm_time(at) \
|
664
|
+
if at.kind_of?(DateTime)
|
665
|
+
|
666
|
+
at = at.to_f \
|
667
|
+
if at.kind_of?(Time)
|
668
|
+
|
669
|
+
#puts "1 at is '#{at.to_s}' (#{at.class})"}"
|
670
|
+
|
671
|
+
jobClass = params[:every] ? EveryJob : AtJob
|
672
|
+
|
673
|
+
job_id = params[:job_id]
|
674
|
+
|
675
|
+
b = to_block(params, &block)
|
676
|
+
|
677
|
+
job = jobClass.new(self, at, job_id, params, &b)
|
678
|
+
|
679
|
+
#do_unschedule(job_id) if job_id
|
680
|
+
|
681
|
+
if at < (Time.new.to_f + @precision)
|
682
|
+
job.trigger() unless params[:discard_past]
|
683
|
+
return nil
|
684
|
+
end
|
685
|
+
|
686
|
+
@schedule_queue << job
|
687
|
+
|
688
|
+
job.job_id
|
689
|
+
end
|
690
|
+
|
691
|
+
#
|
692
|
+
# Ensures that a duration is a expressed as a Float instance.
|
693
|
+
#
|
694
|
+
# duration_to_f("10s")
|
695
|
+
#
|
696
|
+
# will yields 10.0
|
697
|
+
#
|
698
|
+
def duration_to_f (s)
|
699
|
+
|
700
|
+
return s if s.kind_of?(Float)
|
701
|
+
return Rufus::parse_time_string(s) if s.kind_of?(String)
|
702
|
+
Float(s.to_s)
|
703
|
+
end
|
704
|
+
|
705
|
+
#
|
706
|
+
# Returns a block. If a block is passed, will return it, else,
|
707
|
+
# if a :schedulable is set in the params, will return a block
|
708
|
+
# wrapping a call to it.
|
709
|
+
#
|
710
|
+
def to_block (params, &block)
|
711
|
+
|
712
|
+
return block if block
|
713
|
+
|
714
|
+
schedulable = params[:schedulable]
|
715
|
+
|
716
|
+
return nil unless schedulable
|
717
|
+
|
718
|
+
params.delete :schedulable
|
719
|
+
|
720
|
+
l = lambda do
|
721
|
+
schedulable.trigger(params)
|
722
|
+
end
|
723
|
+
class << l
|
724
|
+
attr_accessor :schedulable
|
725
|
+
end
|
726
|
+
l.schedulable = schedulable
|
727
|
+
|
728
|
+
l
|
729
|
+
end
|
730
|
+
|
731
|
+
#
|
732
|
+
# Pushes an 'at' job into the pending job list
|
733
|
+
#
|
734
|
+
def push_pending_job (job)
|
735
|
+
|
736
|
+
old = @pending_jobs.find { |j| j.job_id == job.job_id }
|
737
|
+
@pending_jobs.delete(old) if old
|
738
|
+
#
|
739
|
+
# override previous job with same id
|
740
|
+
|
741
|
+
if @pending_jobs.length < 1 or job.at >= @pending_jobs.last.at
|
742
|
+
@pending_jobs << job
|
743
|
+
return
|
744
|
+
end
|
745
|
+
|
746
|
+
for i in 0...@pending_jobs.length
|
747
|
+
if job.at <= @pending_jobs[i].at
|
748
|
+
@pending_jobs[i, 0] = job
|
749
|
+
return # right place found
|
750
|
+
end
|
751
|
+
end
|
752
|
+
end
|
753
|
+
|
754
|
+
#
|
755
|
+
# This is the method called each time the scheduler wakes up
|
756
|
+
# (by default 4 times per second). It's meant to quickly
|
757
|
+
# determine if there are jobs to trigger else to get back to sleep.
|
758
|
+
# 'cron' jobs get executed if necessary then 'at' jobs.
|
759
|
+
#
|
760
|
+
def step
|
761
|
+
|
762
|
+
#puts Time.now.to_f
|
763
|
+
#puts @pending_jobs.collect { |j| [ j.job_id, j.at ] }.inspect
|
764
|
+
|
765
|
+
step_unschedule
|
766
|
+
# unschedules any job in the unschedule queue before
|
767
|
+
# they have a chance to get triggered.
|
768
|
+
|
769
|
+
step_trigger
|
770
|
+
# triggers eligible jobs
|
771
|
+
|
772
|
+
step_schedule
|
773
|
+
# schedule new jobs
|
774
|
+
|
775
|
+
# done.
|
776
|
+
end
|
777
|
+
|
778
|
+
#
|
779
|
+
# unschedules jobs in the unschedule_queue
|
780
|
+
#
|
781
|
+
def step_unschedule
|
782
|
+
|
783
|
+
loop do
|
784
|
+
|
785
|
+
break if @unschedule_queue.empty?
|
786
|
+
|
787
|
+
type, job_id = @unschedule_queue.pop
|
788
|
+
|
789
|
+
if type == :cron
|
790
|
+
|
791
|
+
do_unschedule_cron_job job_id
|
792
|
+
else
|
793
|
+
|
794
|
+
do_unschedule job_id
|
795
|
+
end
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
#
|
800
|
+
# adds every job waiting in the @schedule_queue to
|
801
|
+
# either @pending_jobs or @cron_jobs.
|
802
|
+
#
|
803
|
+
def step_schedule
|
804
|
+
|
805
|
+
loop do
|
806
|
+
|
807
|
+
break if @schedule_queue.empty?
|
808
|
+
|
809
|
+
j = @schedule_queue.pop
|
810
|
+
|
811
|
+
if j.is_a?(CronJob)
|
812
|
+
|
813
|
+
@cron_jobs[j.job_id] = j
|
814
|
+
|
815
|
+
else # it's an 'at' job
|
816
|
+
|
817
|
+
push_pending_job j
|
818
|
+
end
|
819
|
+
end
|
820
|
+
end
|
821
|
+
|
822
|
+
#
|
823
|
+
# triggers every eligible pending jobs, then every eligible
|
824
|
+
# cron jobs.
|
825
|
+
#
|
826
|
+
def step_trigger
|
827
|
+
|
828
|
+
now = Time.new
|
829
|
+
|
830
|
+
if @exit_when_no_more_jobs
|
831
|
+
|
832
|
+
if @pending_jobs.size < 1
|
833
|
+
|
834
|
+
@stopped = true
|
835
|
+
return
|
836
|
+
end
|
837
|
+
|
838
|
+
@dont_reschedule_every = true if at_job_count < 1
|
839
|
+
end
|
840
|
+
|
841
|
+
# TODO : eventually consider running cron / pending
|
842
|
+
# job triggering in two different threads
|
843
|
+
#
|
844
|
+
# but well... there's the synchronization issue...
|
845
|
+
|
846
|
+
#
|
847
|
+
# cron jobs
|
848
|
+
|
849
|
+
if now.sec != @last_cron_second
|
850
|
+
|
851
|
+
@last_cron_second = now.sec
|
852
|
+
|
853
|
+
#puts "step() @cron_jobs.size #{@cron_jobs.size}"
|
854
|
+
|
855
|
+
@cron_jobs.each do |cron_id, cron_job|
|
856
|
+
#puts "step() cron_id : #{cron_id}"
|
857
|
+
trigger(cron_job) if cron_job.matches?(now)
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
#
|
862
|
+
# pending jobs
|
863
|
+
|
864
|
+
now = now.to_f
|
865
|
+
#
|
866
|
+
# that's what at jobs do understand
|
867
|
+
|
868
|
+
loop do
|
869
|
+
|
870
|
+
break if @pending_jobs.length < 1
|
871
|
+
|
872
|
+
job = @pending_jobs[0]
|
873
|
+
|
874
|
+
break if job.at > now
|
875
|
+
|
876
|
+
#if job.at <= now
|
877
|
+
#
|
878
|
+
# obviously
|
879
|
+
|
880
|
+
trigger job
|
881
|
+
|
882
|
+
@pending_jobs.delete_at 0
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
#
|
887
|
+
# Triggers the job (in a dedicated thread).
|
888
|
+
#
|
889
|
+
def trigger (job)
|
890
|
+
|
891
|
+
Thread.new do
|
892
|
+
begin
|
893
|
+
|
894
|
+
job.trigger
|
895
|
+
|
896
|
+
rescue Exception => e
|
897
|
+
|
898
|
+
log_exception e
|
899
|
+
end
|
900
|
+
end
|
901
|
+
end
|
902
|
+
|
903
|
+
#
|
904
|
+
# If an error occurs in the job, it well get caught and an error
|
905
|
+
# message will be displayed to STDOUT.
|
906
|
+
# If this scheduler provides a lwarn(message) method, it will
|
907
|
+
# be used insted.
|
908
|
+
#
|
909
|
+
# Of course, one can override this method.
|
910
|
+
#
|
911
|
+
def log_exception (e)
|
912
|
+
|
913
|
+
message =
|
914
|
+
"trigger() caught exception\n" +
|
915
|
+
e.to_s + "\n" +
|
916
|
+
e.backtrace.join("\n")
|
917
|
+
|
918
|
+
if self.respond_to?(:lwarn)
|
919
|
+
lwarn { message }
|
920
|
+
else
|
921
|
+
puts message
|
922
|
+
end
|
923
|
+
end
|
924
|
+
end
|
925
|
+
|
926
|
+
#
|
927
|
+
# This module adds a trigger method to any class that includes it.
|
928
|
+
# The default implementation feature here triggers an exception.
|
929
|
+
#
|
930
|
+
module Schedulable
|
931
|
+
|
932
|
+
def trigger (params)
|
933
|
+
raise "trigger() implementation is missing"
|
934
|
+
end
|
935
|
+
|
936
|
+
def reschedule (scheduler)
|
937
|
+
raise "reschedule() implentation is missing"
|
938
|
+
end
|
939
|
+
end
|
940
|
+
|
941
|
+
protected
|
942
|
+
|
943
|
+
JOB_ID_LOCK = Monitor.new
|
944
|
+
#
|
945
|
+
# would it be better to use a Mutex instead of a full-blown
|
946
|
+
# Monitor ?
|
947
|
+
|
948
|
+
#
|
949
|
+
# The parent class for scheduled jobs.
|
950
|
+
#
|
951
|
+
class Job
|
952
|
+
|
953
|
+
@@last_given_id = 0
|
954
|
+
#
|
955
|
+
# as a scheduler is fully transient, no need to
|
956
|
+
# have persistent ids, a simple counter is sufficient
|
957
|
+
|
958
|
+
#
|
959
|
+
# The identifier for the job
|
960
|
+
#
|
961
|
+
attr_accessor :job_id
|
962
|
+
|
963
|
+
#
|
964
|
+
# An array of tags
|
965
|
+
#
|
966
|
+
attr_accessor :tags
|
967
|
+
|
968
|
+
#
|
969
|
+
# The block to execute at trigger time
|
970
|
+
#
|
971
|
+
attr_accessor :block
|
972
|
+
|
973
|
+
#
|
974
|
+
# A reference to the scheduler
|
975
|
+
#
|
976
|
+
attr_reader :scheduler
|
977
|
+
|
978
|
+
#
|
979
|
+
# Keeping a copy of the initialization params of the job.
|
980
|
+
#
|
981
|
+
attr_reader :params
|
982
|
+
|
983
|
+
|
984
|
+
def initialize (scheduler, job_id, params, &block)
|
985
|
+
|
986
|
+
@scheduler = scheduler
|
987
|
+
@block = block
|
988
|
+
|
989
|
+
if job_id
|
990
|
+
@job_id = job_id
|
991
|
+
else
|
992
|
+
JOB_ID_LOCK.synchronize do
|
993
|
+
@job_id = @@last_given_id
|
994
|
+
@@last_given_id = @job_id + 1
|
995
|
+
end
|
996
|
+
end
|
997
|
+
|
998
|
+
@params = params
|
999
|
+
|
1000
|
+
#@tags = Array(tags).collect { |tag| tag.to_s }
|
1001
|
+
# making sure we have an array of String tags
|
1002
|
+
|
1003
|
+
@tags = Array(params[:tags])
|
1004
|
+
# any tag is OK
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
#
|
1008
|
+
# Returns true if this job sports the given tag
|
1009
|
+
#
|
1010
|
+
def has_tag? (tag)
|
1011
|
+
|
1012
|
+
@tags.include?(tag)
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
#
|
1016
|
+
# Removes (cancels) this job from its scheduler.
|
1017
|
+
#
|
1018
|
+
def unschedule
|
1019
|
+
|
1020
|
+
@scheduler.unschedule(@job_id)
|
1021
|
+
end
|
1022
|
+
end
|
1023
|
+
|
1024
|
+
#
|
1025
|
+
# An 'at' job.
|
1026
|
+
#
|
1027
|
+
class AtJob < Job
|
1028
|
+
|
1029
|
+
#
|
1030
|
+
# The float representation (Time.to_f) of the time at which
|
1031
|
+
# the job should be triggered.
|
1032
|
+
#
|
1033
|
+
attr_accessor :at
|
1034
|
+
|
1035
|
+
#
|
1036
|
+
# The constructor.
|
1037
|
+
#
|
1038
|
+
def initialize (scheduler, at, at_id, params, &block)
|
1039
|
+
|
1040
|
+
super(scheduler, at_id, params, &block)
|
1041
|
+
@at = at
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
#
|
1045
|
+
# Triggers the job (calls the block)
|
1046
|
+
#
|
1047
|
+
def trigger
|
1048
|
+
|
1049
|
+
@block.call @job_id, @at
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
#
|
1053
|
+
# Returns the Time instance at which this job is scheduled.
|
1054
|
+
#
|
1055
|
+
def schedule_info
|
1056
|
+
|
1057
|
+
Time.at(@at)
|
1058
|
+
end
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
#
|
1062
|
+
# An 'every' job is simply an extension of an 'at' job.
|
1063
|
+
#
|
1064
|
+
class EveryJob < AtJob
|
1065
|
+
|
1066
|
+
#
|
1067
|
+
# Returns the frequency string used to schedule this EveryJob,
|
1068
|
+
# like for example "3d" or "1M10d3h".
|
1069
|
+
#
|
1070
|
+
def schedule_info
|
1071
|
+
|
1072
|
+
@params[:every]
|
1073
|
+
end
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
#
|
1077
|
+
# A cron job.
|
1078
|
+
#
|
1079
|
+
class CronJob < Job
|
1080
|
+
|
1081
|
+
#
|
1082
|
+
# The CronLine instance representing the times at which
|
1083
|
+
# the cron job has to be triggered.
|
1084
|
+
#
|
1085
|
+
attr_accessor :cron_line
|
1086
|
+
|
1087
|
+
def initialize (scheduler, cron_id, line, params, &block)
|
1088
|
+
|
1089
|
+
super(scheduler, cron_id, params, &block)
|
1090
|
+
|
1091
|
+
if line.is_a?(String)
|
1092
|
+
|
1093
|
+
@cron_line = CronLine.new(line)
|
1094
|
+
|
1095
|
+
elsif line.is_a?(CronLine)
|
1096
|
+
|
1097
|
+
@cron_line = line
|
1098
|
+
|
1099
|
+
else
|
1100
|
+
|
1101
|
+
raise \
|
1102
|
+
"Cannot initialize a CronJob " +
|
1103
|
+
"with a param of class #{line.class}"
|
1104
|
+
end
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
#
|
1108
|
+
# This is the method called by the scheduler to determine if it
|
1109
|
+
# has to fire this CronJob instance.
|
1110
|
+
#
|
1111
|
+
def matches? (time)
|
1112
|
+
|
1113
|
+
@cron_line.matches? time
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
#
|
1117
|
+
# As the name implies.
|
1118
|
+
#
|
1119
|
+
def trigger
|
1120
|
+
|
1121
|
+
@block.call @job_id, @cron_line
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
#
|
1125
|
+
# Returns the original cron tab string used to schedule this
|
1126
|
+
# Job. Like for example "60/3 * * * Sun".
|
1127
|
+
#
|
1128
|
+
def schedule_info
|
1129
|
+
|
1130
|
+
@cron_line.original
|
1131
|
+
end
|
1132
|
+
end
|
1133
|
+
|
1134
|
+
#
|
1135
|
+
# A 'cron line' is a line in the sense of a crontab
|
1136
|
+
# (man 5 crontab) file line.
|
1137
|
+
#
|
1138
|
+
class CronLine
|
1139
|
+
|
1140
|
+
#
|
1141
|
+
# The string used for creating this cronline instance.
|
1142
|
+
#
|
1143
|
+
attr_reader :original
|
1144
|
+
|
1145
|
+
attr_reader \
|
1146
|
+
:seconds,
|
1147
|
+
:minutes,
|
1148
|
+
:hours,
|
1149
|
+
:days,
|
1150
|
+
:months,
|
1151
|
+
:weekdays
|
1152
|
+
|
1153
|
+
def initialize (line)
|
1154
|
+
|
1155
|
+
super()
|
1156
|
+
|
1157
|
+
@original = line
|
1158
|
+
|
1159
|
+
items = line.split
|
1160
|
+
|
1161
|
+
unless [ 5, 6 ].include?(items.length)
|
1162
|
+
raise \
|
1163
|
+
"cron '#{line}' string should hold 5 or 6 items, " +
|
1164
|
+
"not #{items.length}" \
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
offset = items.length - 5
|
1168
|
+
|
1169
|
+
@seconds = if offset == 1
|
1170
|
+
parse_item(items[0], 0, 59)
|
1171
|
+
else
|
1172
|
+
[ 0 ]
|
1173
|
+
end
|
1174
|
+
@minutes = parse_item(items[0+offset], 0, 59)
|
1175
|
+
@hours = parse_item(items[1+offset], 0, 24)
|
1176
|
+
@days = parse_item(items[2+offset], 1, 31)
|
1177
|
+
@months = parse_item(items[3+offset], 1, 12)
|
1178
|
+
@weekdays = parse_weekdays(items[4+offset])
|
1179
|
+
|
1180
|
+
#adjust_arrays()
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
#
|
1184
|
+
# Returns true if the given time matches this cron line.
|
1185
|
+
#
|
1186
|
+
def matches? (time)
|
1187
|
+
|
1188
|
+
time = Time.at(time) \
|
1189
|
+
if time.kind_of?(Float) or time.kind_of?(Integer)
|
1190
|
+
|
1191
|
+
return false if no_match?(time.sec, @seconds)
|
1192
|
+
return false if no_match?(time.min, @minutes)
|
1193
|
+
return false if no_match?(time.hour, @hours)
|
1194
|
+
return false if no_match?(time.day, @days)
|
1195
|
+
return false if no_match?(time.month, @months)
|
1196
|
+
return false if no_match?(time.wday, @weekdays)
|
1197
|
+
|
1198
|
+
true
|
1199
|
+
end
|
1200
|
+
|
1201
|
+
#
|
1202
|
+
# Returns an array of 6 arrays (seconds, minutes, hours, days,
|
1203
|
+
# months, weekdays).
|
1204
|
+
# This method is used by the cronline unit tests.
|
1205
|
+
#
|
1206
|
+
def to_array
|
1207
|
+
[ @seconds, @minutes, @hours, @days, @months, @weekdays ]
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
private
|
1211
|
+
|
1212
|
+
#--
|
1213
|
+
# adjust values to Ruby
|
1214
|
+
#
|
1215
|
+
#def adjust_arrays()
|
1216
|
+
# @hours = @hours.collect { |h|
|
1217
|
+
# if h == 24
|
1218
|
+
# 0
|
1219
|
+
# else
|
1220
|
+
# h
|
1221
|
+
# end
|
1222
|
+
# } if @hours
|
1223
|
+
# @weekdays = @weekdays.collect { |wd|
|
1224
|
+
# wd - 1
|
1225
|
+
# } if @weekdays
|
1226
|
+
#end
|
1227
|
+
#
|
1228
|
+
# dead code, keeping it as a reminder
|
1229
|
+
#++
|
1230
|
+
|
1231
|
+
WDS = [ "mon", "tue", "wed", "thu", "fri", "sat", "sun" ]
|
1232
|
+
#
|
1233
|
+
# used by parse_weekday()
|
1234
|
+
|
1235
|
+
def parse_weekdays (item)
|
1236
|
+
|
1237
|
+
item = item.downcase
|
1238
|
+
|
1239
|
+
WDS.each_with_index do |day, index|
|
1240
|
+
item = item.gsub(day, "#{index+1}")
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
parse_item(item, 1, 7)
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
def parse_item (item, min, max)
|
1247
|
+
|
1248
|
+
return nil \
|
1249
|
+
if item == "*"
|
1250
|
+
return parse_list(item, min, max) \
|
1251
|
+
if item.index(",")
|
1252
|
+
return parse_range(item, min, max) \
|
1253
|
+
if item.index("*") or item.index("-")
|
1254
|
+
|
1255
|
+
i = Integer(item)
|
1256
|
+
|
1257
|
+
i = min if i < min
|
1258
|
+
i = max if i > max
|
1259
|
+
|
1260
|
+
[ i ]
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
def parse_list (item, min, max)
|
1264
|
+
|
1265
|
+
items = item.split(",")
|
1266
|
+
|
1267
|
+
items.inject([]) do |result, i|
|
1268
|
+
|
1269
|
+
i = Integer(i)
|
1270
|
+
|
1271
|
+
i = min if i < min
|
1272
|
+
i = max if i > max
|
1273
|
+
|
1274
|
+
result.push i
|
1275
|
+
end
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
def parse_range (item, min, max)
|
1279
|
+
|
1280
|
+
i = item.index("-")
|
1281
|
+
j = item.index("/")
|
1282
|
+
|
1283
|
+
inc = 1
|
1284
|
+
|
1285
|
+
inc = Integer(item[j+1..-1]) if j
|
1286
|
+
|
1287
|
+
istart = -1
|
1288
|
+
iend = -1
|
1289
|
+
|
1290
|
+
if i
|
1291
|
+
|
1292
|
+
istart = Integer(item[0..i-1])
|
1293
|
+
|
1294
|
+
if j
|
1295
|
+
iend = Integer(item[i+1..j])
|
1296
|
+
else
|
1297
|
+
iend = Integer(item[i+1..-1])
|
1298
|
+
end
|
1299
|
+
|
1300
|
+
else # case */x
|
1301
|
+
|
1302
|
+
istart = min
|
1303
|
+
iend = max
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
istart = min if istart < min
|
1307
|
+
iend = max if iend > max
|
1308
|
+
|
1309
|
+
result = []
|
1310
|
+
|
1311
|
+
value = istart
|
1312
|
+
loop do
|
1313
|
+
|
1314
|
+
result << value
|
1315
|
+
value = value + inc
|
1316
|
+
break if value > iend
|
1317
|
+
end
|
1318
|
+
|
1319
|
+
result
|
1320
|
+
end
|
1321
|
+
|
1322
|
+
def no_match? (value, cron_values)
|
1323
|
+
|
1324
|
+
return false if not cron_values
|
1325
|
+
|
1326
|
+
cron_values.each do |v|
|
1327
|
+
return false if value == v
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
true
|
1331
|
+
end
|
1332
|
+
end
|
1333
|
+
|
1334
|
+
end
|
1335
|
+
|