openwferu-scheduler 0.9.7
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/openwfe/util/otime.rb +244 -0
- data/lib/openwfe/util/scheduler.rb +840 -0
- metadata +47 -0
@@ -0,0 +1,244 @@
|
|
1
|
+
#
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2005-2007, John Mettraux, OpenWFE.org
|
4
|
+
# All rights reserved.
|
5
|
+
#
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
8
|
+
#
|
9
|
+
# . Redistributions of source code must retain the above copyright notice, this
|
10
|
+
# list of conditions and the following disclaimer.
|
11
|
+
#
|
12
|
+
# . Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
14
|
+
# and/or other materials provided with the distribution.
|
15
|
+
#
|
16
|
+
# . Neither the name of the "OpenWFE" nor the names of its contributors may be
|
17
|
+
# used to endorse or promote products derived from this software without
|
18
|
+
# specific prior written permission.
|
19
|
+
#
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
23
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
24
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
25
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
26
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
27
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
28
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
29
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
30
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
31
|
+
#++
|
32
|
+
#
|
33
|
+
# $Id: otime.rb 3509 2006-10-21 12:00:52Z jmettraux $
|
34
|
+
#
|
35
|
+
|
36
|
+
#
|
37
|
+
# "hecho en Costa Rica"
|
38
|
+
#
|
39
|
+
# john.mettraux@openwfe.org
|
40
|
+
#
|
41
|
+
|
42
|
+
require 'date'
|
43
|
+
#require 'parsedate'
|
44
|
+
|
45
|
+
|
46
|
+
module OpenWFE
|
47
|
+
|
48
|
+
#TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
|
49
|
+
|
50
|
+
#
|
51
|
+
# Returns the current time as an ISO date string
|
52
|
+
#
|
53
|
+
def OpenWFE.now ()
|
54
|
+
return to_iso8601_date(Time.new())
|
55
|
+
end
|
56
|
+
|
57
|
+
def OpenWFE.to_iso8601_date (date)
|
58
|
+
|
59
|
+
if date.kind_of? Float
|
60
|
+
date = to_datetime(Time.at(date))
|
61
|
+
elsif date.kind_of? Time
|
62
|
+
date = to_datetime(date)
|
63
|
+
elsif not date.kind_of? Date
|
64
|
+
date = DateTime.parse(date)
|
65
|
+
end
|
66
|
+
|
67
|
+
s = date.to_s
|
68
|
+
s[10] = " "
|
69
|
+
|
70
|
+
return s
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# the old method we used to generate our ISO datetime strings
|
75
|
+
#
|
76
|
+
def OpenWFE.time_to_iso8601_date (time)
|
77
|
+
|
78
|
+
s = time.getutc().strftime(TIME_FORMAT)
|
79
|
+
o = time.utc_offset / 3600
|
80
|
+
o = o.to_s + "00"
|
81
|
+
o = "0" + o if o.length < 4
|
82
|
+
o = "+" + o unless o[0..1] == '-'
|
83
|
+
|
84
|
+
s + " " + o.to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
#
|
88
|
+
# Returns a Ruby time
|
89
|
+
#
|
90
|
+
def OpenWFE.to_ruby_time (iso_date)
|
91
|
+
|
92
|
+
return DateTime.parse(iso_date)
|
93
|
+
end
|
94
|
+
|
95
|
+
#def OpenWFE.parse_date (date)
|
96
|
+
#end
|
97
|
+
|
98
|
+
#
|
99
|
+
# equivalent to java.lang.System.currentTimeMillis()
|
100
|
+
#
|
101
|
+
def OpenWFE.current_time_millis ()
|
102
|
+
|
103
|
+
t = Time.new()
|
104
|
+
t = t.to_f * 1000
|
105
|
+
return t.to_i
|
106
|
+
end
|
107
|
+
|
108
|
+
#
|
109
|
+
# turns a string like '1m10s' into a float like '70.0'
|
110
|
+
#
|
111
|
+
# w -> week
|
112
|
+
# d -> day
|
113
|
+
# h -> hour
|
114
|
+
# m -> minute
|
115
|
+
# s -> second
|
116
|
+
# M -> month
|
117
|
+
# y -> year
|
118
|
+
# 'nada' -> millisecond
|
119
|
+
#
|
120
|
+
def OpenWFE.parse_time_string (string)
|
121
|
+
|
122
|
+
string = string.strip
|
123
|
+
|
124
|
+
index = -1
|
125
|
+
result = 0.0
|
126
|
+
|
127
|
+
number = ""
|
128
|
+
|
129
|
+
while true
|
130
|
+
index = index + 1
|
131
|
+
|
132
|
+
if index >= string.length
|
133
|
+
if number.length > 0
|
134
|
+
result = result + (Float(number) / 1000.0)
|
135
|
+
end
|
136
|
+
break
|
137
|
+
end
|
138
|
+
|
139
|
+
c = string[index, 1]
|
140
|
+
|
141
|
+
if is_digit?(c)
|
142
|
+
number = number + c
|
143
|
+
next
|
144
|
+
end
|
145
|
+
|
146
|
+
value = Integer(number)
|
147
|
+
number = ""
|
148
|
+
|
149
|
+
multiplier = DURATIONS[c]
|
150
|
+
|
151
|
+
raise "unknown time char '#{c}'" \
|
152
|
+
if not multiplier
|
153
|
+
|
154
|
+
result = result + (value * multiplier)
|
155
|
+
end
|
156
|
+
|
157
|
+
return result
|
158
|
+
end
|
159
|
+
|
160
|
+
#
|
161
|
+
# returns true if the character c is a digit
|
162
|
+
#
|
163
|
+
def OpenWFE.is_digit? (c)
|
164
|
+
return false if not c.kind_of?(String)
|
165
|
+
return false if c.length > 1
|
166
|
+
return (c >= "0" and c <= "9")
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# conversion methods between Date[Time] and Time
|
171
|
+
|
172
|
+
#
|
173
|
+
# Ruby Cookbook 1st edition p.111
|
174
|
+
# http://www.oreilly.com/catalog/rubyckbk/
|
175
|
+
# a must
|
176
|
+
#
|
177
|
+
|
178
|
+
#
|
179
|
+
# converts a Time instance to a DateTime one
|
180
|
+
#
|
181
|
+
def OpenWFE.to_datetime (time)
|
182
|
+
|
183
|
+
s = time.sec + Rational(time.usec, 10**6)
|
184
|
+
o = Rational(time.utc_offset, 3600 * 24)
|
185
|
+
|
186
|
+
begin
|
187
|
+
|
188
|
+
return DateTime.new(
|
189
|
+
time.year,
|
190
|
+
time.month,
|
191
|
+
time.day,
|
192
|
+
time.hour,
|
193
|
+
time.min,
|
194
|
+
s,
|
195
|
+
o)
|
196
|
+
|
197
|
+
rescue Exception => e
|
198
|
+
|
199
|
+
#puts
|
200
|
+
#puts OpenWFE::exception_to_s(e)
|
201
|
+
#puts
|
202
|
+
#puts \
|
203
|
+
# "\n Date.new() problem. Params :"+
|
204
|
+
# "\n....y:#{time.year} M:#{time.month} d:#{time.day} "+
|
205
|
+
# "h:#{time.hour} m:#{time.min} s:#{s} o:#{o}"
|
206
|
+
|
207
|
+
return DateTime.new(
|
208
|
+
time.year,
|
209
|
+
time.month,
|
210
|
+
time.day,
|
211
|
+
time.hour,
|
212
|
+
time.min,
|
213
|
+
time.sec,
|
214
|
+
time.utc_offset)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def OpenWFE.to_gm_time (dtime)
|
219
|
+
to_ttime(dtime.new_offset, :gm)
|
220
|
+
end
|
221
|
+
|
222
|
+
def OpenWFE.to_local_time (dtime)
|
223
|
+
to_ttime(dtime.new_offset(DateTime.now.offset-offset), :local)
|
224
|
+
end
|
225
|
+
|
226
|
+
def to_ttime (d, method)
|
227
|
+
usec = (d.sec_fraction * 3600 * 24 * (10**6)).to_i
|
228
|
+
Time.send(method, d.year, d.month, d.day, d.hour, d.min, d.sec, usec)
|
229
|
+
end
|
230
|
+
|
231
|
+
protected
|
232
|
+
|
233
|
+
DURATIONS = {
|
234
|
+
"y" => 365 * 24 * 3600,
|
235
|
+
"M" => 30 * 24 * 3600,
|
236
|
+
"w" => 7 * 24 * 3600,
|
237
|
+
"d" => 24 * 3600,
|
238
|
+
"h" => 3600,
|
239
|
+
"m" => 60,
|
240
|
+
"s" => 1
|
241
|
+
}
|
242
|
+
|
243
|
+
end
|
244
|
+
|
@@ -0,0 +1,840 @@
|
|
1
|
+
#
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2006-2007, John Mettraux, OpenWFE.org
|
4
|
+
# All rights reserved.
|
5
|
+
#
|
6
|
+
# Redistribution and use in source and binary forms, with or without
|
7
|
+
# modification, are permitted provided that the following conditions are met:
|
8
|
+
#
|
9
|
+
# . Redistributions of source code must retain the above copyright notice, this
|
10
|
+
# list of conditions and the following disclaimer.
|
11
|
+
#
|
12
|
+
# . Redistributions in binary form must reproduce the above copyright notice,
|
13
|
+
# this list of conditions and the following disclaimer in the documentation
|
14
|
+
# and/or other materials provided with the distribution.
|
15
|
+
#
|
16
|
+
# . Neither the name of the "OpenWFE" nor the names of its contributors may be
|
17
|
+
# used to endorse or promote products derived from this software without
|
18
|
+
# specific prior written permission.
|
19
|
+
#
|
20
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
21
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
22
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
23
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
24
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
25
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
26
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
27
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
28
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
29
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
30
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
31
|
+
#++
|
32
|
+
#
|
33
|
+
# $Id: definitions.rb 2725 2006-06-02 13:26:32Z jmettraux $
|
34
|
+
#
|
35
|
+
|
36
|
+
#
|
37
|
+
# "made in Japan"
|
38
|
+
#
|
39
|
+
# John Mettraux at openwfe.org
|
40
|
+
#
|
41
|
+
|
42
|
+
require 'monitor'
|
43
|
+
|
44
|
+
require 'openwfe/util/otime'
|
45
|
+
|
46
|
+
|
47
|
+
module OpenWFE
|
48
|
+
|
49
|
+
#
|
50
|
+
# The Scheduler is used by OpenWFEru for registering 'at' and 'cron' jobs.
|
51
|
+
# 'at' jobs to execute once at a given point in time. 'cron' jobs
|
52
|
+
# execute a specified intervals.
|
53
|
+
# The two main methods are thus schedule_at() and schedule().
|
54
|
+
#
|
55
|
+
# schedule_at() and schedule() await either a Schedulable instance and
|
56
|
+
# params (usually an array or nil), either a block, which is more in the
|
57
|
+
# Ruby way.
|
58
|
+
#
|
59
|
+
# Two examples :
|
60
|
+
#
|
61
|
+
# scheduler.schedule_in("3d") do
|
62
|
+
# regenerate_monthly_report()
|
63
|
+
# end
|
64
|
+
# #
|
65
|
+
# # will call the regenerate_monthly_report method
|
66
|
+
# # in 3 days from now
|
67
|
+
#
|
68
|
+
# and
|
69
|
+
#
|
70
|
+
# class Regenerator < Schedulable
|
71
|
+
# def trigger (frequency)
|
72
|
+
# self.send(frequency)
|
73
|
+
# end
|
74
|
+
# def monthly
|
75
|
+
# # ...
|
76
|
+
# end
|
77
|
+
# def yearly
|
78
|
+
# # ...
|
79
|
+
# end
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# regenerator = Regenerator.new
|
83
|
+
#
|
84
|
+
# scheduler.schedule_in("4d", regenerator, :monthly)
|
85
|
+
# #
|
86
|
+
# # will regenerate the monthly report in four days
|
87
|
+
#
|
88
|
+
# There is also schedule_every() :
|
89
|
+
#
|
90
|
+
# scheduler.schedule_every("1h20m") do
|
91
|
+
# regenerate_latest_report()
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# The scheduler has a "exit_when_no_more_jobs" attribute. When set to
|
95
|
+
# 'true', the scheduler will exit as soon as there are no more jobs to
|
96
|
+
# run.
|
97
|
+
# Use with care though, if you create a scheduler, set this attribute
|
98
|
+
# to true and start the scheduler, the scheduler will immediately exit.
|
99
|
+
# This attribute is best used indirectly : the method
|
100
|
+
# join_until_no_more_jobs() wraps it.
|
101
|
+
#
|
102
|
+
class Scheduler
|
103
|
+
include MonitorMixin
|
104
|
+
|
105
|
+
attr_accessor \
|
106
|
+
:precision,
|
107
|
+
:exit_when_no_more_jobs
|
108
|
+
|
109
|
+
def initialize
|
110
|
+
|
111
|
+
super()
|
112
|
+
|
113
|
+
@pending_jobs = []
|
114
|
+
@cron_entries = {}
|
115
|
+
|
116
|
+
@scheduler_thread = nil
|
117
|
+
|
118
|
+
@precision = 0.250
|
119
|
+
#
|
120
|
+
# every 250ms, the scheduler wakes up
|
121
|
+
|
122
|
+
@exit_when_no_more_jobs = false
|
123
|
+
@dont_reschedule_every = false
|
124
|
+
|
125
|
+
@last_cron_minute = -1
|
126
|
+
|
127
|
+
@stopped = false
|
128
|
+
end
|
129
|
+
|
130
|
+
#
|
131
|
+
# Starts this scheduler (or restart it if it was previously stopped)
|
132
|
+
#
|
133
|
+
def sstart
|
134
|
+
|
135
|
+
@scheduler_thread = Thread.new do
|
136
|
+
while true
|
137
|
+
break if @stopped
|
138
|
+
step
|
139
|
+
sleep(@precision)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
#
|
145
|
+
# The scheduler is stoppable via sstop()
|
146
|
+
#
|
147
|
+
def sstop
|
148
|
+
|
149
|
+
@stopped = true
|
150
|
+
end
|
151
|
+
|
152
|
+
alias :start :sstart
|
153
|
+
alias :stop :sstop
|
154
|
+
|
155
|
+
#
|
156
|
+
# Joins on the scheduler thread
|
157
|
+
#
|
158
|
+
def join
|
159
|
+
|
160
|
+
@scheduler_thread.join
|
161
|
+
end
|
162
|
+
|
163
|
+
#
|
164
|
+
# Like join() but takes care of setting the 'exit_when_no_more_jobs'
|
165
|
+
# attribute of this scheduler to true before joining.
|
166
|
+
# Thus the scheduler will exit (and the join terminates) as soon as
|
167
|
+
# there aren't no more 'at' (or 'every') jobs in the scheduler.
|
168
|
+
#
|
169
|
+
# Currently used only in unit tests.
|
170
|
+
#
|
171
|
+
def join_until_no_more_jobs
|
172
|
+
|
173
|
+
@exit_when_no_more_jobs = true
|
174
|
+
join
|
175
|
+
end
|
176
|
+
|
177
|
+
#
|
178
|
+
# Schedules a job by specifying at which time it should trigger.
|
179
|
+
# Returns the a job_id that can be used to unschedule the job.
|
180
|
+
#
|
181
|
+
def schedule_at (at, schedulable=nil, params=nil, &block)
|
182
|
+
|
183
|
+
sschedule_at(false, at, nil, schedulable, params, &block)
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
#
|
188
|
+
# Schedules a job by stating in how much time it should trigger.
|
189
|
+
# Returns the a job_id that can be used to unschedule the job.
|
190
|
+
#
|
191
|
+
def schedule_in (duration, schedulable=nil, params=nil, &block)
|
192
|
+
|
193
|
+
duration = duration_to_f(duration)
|
194
|
+
|
195
|
+
return schedule_at(
|
196
|
+
Time.new.to_f + duration, schedulable, params, &block)
|
197
|
+
end
|
198
|
+
|
199
|
+
#
|
200
|
+
# Schedules a job in a loop. After an execution, it will not execute
|
201
|
+
# before the time specified in 'freq'.
|
202
|
+
#
|
203
|
+
# Note that if your job takes 2s to execute and the freq is set to
|
204
|
+
# 10s, it will in fact execute every 12s.
|
205
|
+
# You can however wrap the code within its own thread :
|
206
|
+
#
|
207
|
+
# scheduler.schedule_every("12s") do
|
208
|
+
# Thread.new do
|
209
|
+
# do_the_job()
|
210
|
+
# end
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
def schedule_every (freq, schedulable=nil, params=nil, &block)
|
214
|
+
|
215
|
+
sschedule_every(freq, nil, schedulable, params, &block)
|
216
|
+
end
|
217
|
+
|
218
|
+
#
|
219
|
+
# Unschedules an 'at' or a 'cron' job identified by the id
|
220
|
+
# it was given at schedule time.
|
221
|
+
#
|
222
|
+
def unschedule (job_id)
|
223
|
+
synchronize do
|
224
|
+
|
225
|
+
for i in 0...@pending_jobs.length
|
226
|
+
if @pending_jobs[i].eid == job_id
|
227
|
+
@pending_jobs.delete_at(i)
|
228
|
+
return true
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
return true if unschedule_cron_job(job_id)
|
233
|
+
|
234
|
+
return false
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
#
|
239
|
+
# Unschedules a cron job
|
240
|
+
#
|
241
|
+
def unschedule_cron_job (job_id)
|
242
|
+
synchronize do
|
243
|
+
if @cron_entries.has_key?(job_id)
|
244
|
+
@cron_entries.delete(job_id)
|
245
|
+
return true
|
246
|
+
end
|
247
|
+
return false
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
#
|
252
|
+
# Schedules a cron job, the 'cron_line' is a string
|
253
|
+
# following the Unix cron standard (see "man 5 crontab" in your command
|
254
|
+
# line).
|
255
|
+
#
|
256
|
+
# For example :
|
257
|
+
#
|
258
|
+
# scheduler.schedule("5 0 * * *", nil, s, p)
|
259
|
+
# # will trigger the schedulable s with params p every day
|
260
|
+
# # five minutes after midnight
|
261
|
+
#
|
262
|
+
# scheduler.schedule("15 14 1 * *", nil, s, p)
|
263
|
+
# # will trigger s at 14:15 on the first of every month
|
264
|
+
#
|
265
|
+
# scheduler.schedule("0 22 * * 1-5") do
|
266
|
+
# puts "it's break time..."
|
267
|
+
# end
|
268
|
+
# # outputs a message every weekday at 10pm
|
269
|
+
#
|
270
|
+
# Returns the job id attributed to this 'cron job', this id can
|
271
|
+
# be used to unschedule the job.
|
272
|
+
#
|
273
|
+
def schedule (
|
274
|
+
cron_line, cron_id=nil, schedulable=nil, params=nil, &block)
|
275
|
+
|
276
|
+
synchronize do
|
277
|
+
|
278
|
+
#
|
279
|
+
# is a job with the same id already scheduled ?
|
280
|
+
|
281
|
+
if cron_id and unschedule(cron_id)
|
282
|
+
ldebug do
|
283
|
+
"schedule() unscheduled previous job "+
|
284
|
+
"under same name '#{cron_id}'"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
#
|
289
|
+
# schedule
|
290
|
+
|
291
|
+
b = to_block(schedulable, params, &block)
|
292
|
+
entry = CronEntry.new(cron_id, cron_line, &b)
|
293
|
+
@cron_entries[entry.eid] = entry
|
294
|
+
|
295
|
+
return entry.eid
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
#
|
300
|
+
# Returns the job corresponding to job_id, an instance of AtEntry
|
301
|
+
# or CronEntry will be returned.
|
302
|
+
#
|
303
|
+
def get_job (job_id)
|
304
|
+
|
305
|
+
entry = @cron_entries[job_id]
|
306
|
+
return entry if entry
|
307
|
+
|
308
|
+
@pending_jobs.each do |entry|
|
309
|
+
return entry if entry.eid == job_id
|
310
|
+
end
|
311
|
+
|
312
|
+
return nil
|
313
|
+
end
|
314
|
+
|
315
|
+
#
|
316
|
+
# Finds a job (via get_job()) and then returns the wrapped
|
317
|
+
# schedulable if any.
|
318
|
+
#
|
319
|
+
def get_schedulable (job_id)
|
320
|
+
|
321
|
+
return nil unless job_id
|
322
|
+
|
323
|
+
j = get_job(job_id)
|
324
|
+
|
325
|
+
return j.schedulable if j.respond_to? :schedulable
|
326
|
+
return nil
|
327
|
+
end
|
328
|
+
|
329
|
+
#
|
330
|
+
# Returns the number of currently pending jobs in this scheduler
|
331
|
+
# ('at' jobs and 'every' jobs).
|
332
|
+
#
|
333
|
+
def pending_job_count
|
334
|
+
@pending_jobs.size
|
335
|
+
end
|
336
|
+
|
337
|
+
#
|
338
|
+
# Returns the number of cron jobs currently active in this scheduler.
|
339
|
+
#
|
340
|
+
def cron_job_count
|
341
|
+
@cron_entries.size
|
342
|
+
end
|
343
|
+
|
344
|
+
#
|
345
|
+
# Returns the current count of 'every' jobs scheduled.
|
346
|
+
#
|
347
|
+
def every_job_count
|
348
|
+
@pending_jobs.select { |j| j.is_a?(EveryEntry) }.size
|
349
|
+
end
|
350
|
+
|
351
|
+
#
|
352
|
+
# Returns the current count of 'at' jobs scheduled (not 'every').
|
353
|
+
#
|
354
|
+
def at_job_count
|
355
|
+
@pending_jobs.select { |j| j.instance_of?(AtEntry) }.size
|
356
|
+
end
|
357
|
+
|
358
|
+
#
|
359
|
+
# Returns true if the given string seems to be a cron string.
|
360
|
+
#
|
361
|
+
def Scheduler.is_cron_string (s)
|
362
|
+
return s.match(".+ .+ .+ .+ .+")
|
363
|
+
end
|
364
|
+
|
365
|
+
protected
|
366
|
+
|
367
|
+
def sschedule_at (
|
368
|
+
is_every, at, at_id, schedulable=nil, params=nil, &block)
|
369
|
+
|
370
|
+
synchronize do
|
371
|
+
|
372
|
+
#puts "0 at is '#{at.to_s}' (#{at.class})"
|
373
|
+
|
374
|
+
at = OpenWFE::to_ruby_time(at) \
|
375
|
+
if at.kind_of? String
|
376
|
+
|
377
|
+
at = OpenWFE::to_gm_time(at) \
|
378
|
+
if at.kind_of? DateTime
|
379
|
+
|
380
|
+
at = at.to_f \
|
381
|
+
if at.kind_of? Time
|
382
|
+
|
383
|
+
#puts "1 at is '#{at.to_s}' (#{at.class})"}"
|
384
|
+
|
385
|
+
jobClass = AtEntry
|
386
|
+
jobClass = EveryEntry if is_every
|
387
|
+
|
388
|
+
b = to_block(schedulable, params, &block)
|
389
|
+
job = jobClass.new(at, at_id, &b)
|
390
|
+
|
391
|
+
if at < (Time.new.to_f + @precision)
|
392
|
+
job.trigger()
|
393
|
+
return nil
|
394
|
+
end
|
395
|
+
|
396
|
+
return push(job) \
|
397
|
+
if @pending_jobs.length < 1
|
398
|
+
|
399
|
+
# shortcut : check if the new job is posterior to
|
400
|
+
# the last job pending
|
401
|
+
|
402
|
+
return push(job) \
|
403
|
+
if at >= @pending_jobs.last.at
|
404
|
+
|
405
|
+
for i in 0...@pending_jobs.length
|
406
|
+
if at <= @pending_jobs[i].at
|
407
|
+
return push(job, i)
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
return push(job)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def sschedule_every (freq, at_id, schedulable, params, &block)
|
416
|
+
|
417
|
+
f = duration_to_f(freq)
|
418
|
+
|
419
|
+
job_id = sschedule_at(
|
420
|
+
true, Time.new.to_f + f, at_id) do |eid, at|
|
421
|
+
|
422
|
+
if schedulable
|
423
|
+
schedulable.trigger(params)
|
424
|
+
else
|
425
|
+
block.call eid, at
|
426
|
+
end
|
427
|
+
|
428
|
+
sschedule_every(f, eid, schedulable, params, &block) \
|
429
|
+
unless @dont_reschedule_every
|
430
|
+
end
|
431
|
+
|
432
|
+
job_id
|
433
|
+
end
|
434
|
+
|
435
|
+
#
|
436
|
+
# Ensures that a duration is a expressed as a Float instance.
|
437
|
+
#
|
438
|
+
# duration_to_f("10s")
|
439
|
+
#
|
440
|
+
# will yields 10.0
|
441
|
+
#
|
442
|
+
def duration_to_f (s)
|
443
|
+
return s if s.kind_of? Float
|
444
|
+
return OpenWFE::parse_time_string(s) if s.kind_of? String
|
445
|
+
return Float(s.to_s)
|
446
|
+
end
|
447
|
+
|
448
|
+
def to_block (schedulable, params, &block)
|
449
|
+
if schedulable
|
450
|
+
l = lambda do
|
451
|
+
schedulable.trigger(params)
|
452
|
+
end
|
453
|
+
class << l
|
454
|
+
attr_accessor :schedulable
|
455
|
+
end
|
456
|
+
l.schedulable = schedulable
|
457
|
+
l
|
458
|
+
else
|
459
|
+
block
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
#
|
464
|
+
# Pushes an 'at' job into the pending job list
|
465
|
+
#
|
466
|
+
def push (job, index=-1)
|
467
|
+
|
468
|
+
if index == -1
|
469
|
+
#
|
470
|
+
# push job at the end
|
471
|
+
#
|
472
|
+
@pending_jobs << job
|
473
|
+
else
|
474
|
+
#
|
475
|
+
# insert job at given index
|
476
|
+
#
|
477
|
+
@pending_jobs[index, 0] = job
|
478
|
+
end
|
479
|
+
|
480
|
+
#puts "push() at '#{Time.at(job.at)}'"
|
481
|
+
|
482
|
+
return job.eid
|
483
|
+
end
|
484
|
+
|
485
|
+
#
|
486
|
+
# This is the method called each time the scheduler wakes up
|
487
|
+
# (by default 4 times per second). It's meant to quickly
|
488
|
+
# determine if there are jobs to trigger else to get back to sleep.
|
489
|
+
# 'cron' jobs get executed if necessary then 'at' jobs.
|
490
|
+
#
|
491
|
+
def step
|
492
|
+
synchronize do
|
493
|
+
|
494
|
+
now = Time.new
|
495
|
+
minute = now.min
|
496
|
+
|
497
|
+
if @exit_when_no_more_jobs
|
498
|
+
|
499
|
+
if @pending_jobs.size < 1
|
500
|
+
|
501
|
+
@stopped = true
|
502
|
+
return
|
503
|
+
end
|
504
|
+
|
505
|
+
@dont_reschedule_every = true if at_job_count < 1
|
506
|
+
end
|
507
|
+
|
508
|
+
#
|
509
|
+
# cron entries
|
510
|
+
|
511
|
+
if now.sec == 0 and minute > @last_cron_minute
|
512
|
+
#
|
513
|
+
# only consider cron entries at the second 0 of a
|
514
|
+
# minute
|
515
|
+
|
516
|
+
@last_cron_minute = minute
|
517
|
+
|
518
|
+
@cron_entries.each do |cron_id, cron_entry|
|
519
|
+
#puts "step() cron_id : #{cron_id}"
|
520
|
+
trigger(cron_entry) if cron_entry.matches? now
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
#
|
525
|
+
# pending jobs
|
526
|
+
|
527
|
+
now = now.to_f
|
528
|
+
#
|
529
|
+
# that's what at jobs do understand
|
530
|
+
|
531
|
+
while true
|
532
|
+
|
533
|
+
#puts "step() job.count is #{@pending_jobs.length}"
|
534
|
+
|
535
|
+
break if @pending_jobs.length < 1
|
536
|
+
|
537
|
+
job = @pending_jobs[0]
|
538
|
+
|
539
|
+
#puts "step() job.at is #{job.at}"
|
540
|
+
#puts "step() now is #{now}"
|
541
|
+
|
542
|
+
break if job.at > now
|
543
|
+
|
544
|
+
#if job.at <= now
|
545
|
+
#
|
546
|
+
# obviously
|
547
|
+
|
548
|
+
trigger(job)
|
549
|
+
|
550
|
+
@pending_jobs.delete_at(0)
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
def trigger (entry)
|
556
|
+
Thread.new do
|
557
|
+
begin
|
558
|
+
entry.trigger
|
559
|
+
rescue Exception => e
|
560
|
+
message =
|
561
|
+
"trigger() caught exception\n" +
|
562
|
+
OpenWFE::exception_to_s(e)
|
563
|
+
if self.respond_to? :lwarn
|
564
|
+
lwarn { message }
|
565
|
+
else
|
566
|
+
puts message
|
567
|
+
end
|
568
|
+
end
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
#
|
574
|
+
# This module adds a trigger method to any class that includes it.
|
575
|
+
# The default implementation feature here triggers an exception.
|
576
|
+
#
|
577
|
+
module Schedulable
|
578
|
+
|
579
|
+
def trigger (params)
|
580
|
+
raise "trigger() implementation is missing"
|
581
|
+
end
|
582
|
+
|
583
|
+
def reschedule (scheduler)
|
584
|
+
raise "reschedule() implentation is missing"
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
protected
|
589
|
+
|
590
|
+
JOB_ID_LOCK = Monitor.new
|
591
|
+
|
592
|
+
class Entry
|
593
|
+
|
594
|
+
@@last_given_id = 0
|
595
|
+
#
|
596
|
+
# as a scheduler is fully transient, no need to
|
597
|
+
# have persistent ids, a simple counter is sufficient
|
598
|
+
|
599
|
+
attr_accessor \
|
600
|
+
:eid, :block
|
601
|
+
|
602
|
+
def initialize (entry_id=nil, &block)
|
603
|
+
@block = block
|
604
|
+
if entry_id
|
605
|
+
@eid = entry_id
|
606
|
+
else
|
607
|
+
JOB_ID_LOCK.synchronize do
|
608
|
+
@eid = @@last_given_id
|
609
|
+
@@last_given_id = @eid + 1
|
610
|
+
end
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
#def trigger
|
615
|
+
# @block.call @eid
|
616
|
+
#end
|
617
|
+
end
|
618
|
+
|
619
|
+
class AtEntry < Entry
|
620
|
+
|
621
|
+
attr_accessor \
|
622
|
+
:at
|
623
|
+
|
624
|
+
def initialize (at, at_id, &block)
|
625
|
+
super(at_id, &block)
|
626
|
+
@at = at
|
627
|
+
end
|
628
|
+
|
629
|
+
def trigger
|
630
|
+
@block.call @eid, @at
|
631
|
+
end
|
632
|
+
end
|
633
|
+
|
634
|
+
class EveryEntry < AtEntry
|
635
|
+
end
|
636
|
+
|
637
|
+
class CronEntry < Entry
|
638
|
+
|
639
|
+
attr_accessor \
|
640
|
+
:cron_line
|
641
|
+
|
642
|
+
def initialize (cron_id, line, &block)
|
643
|
+
|
644
|
+
super(cron_id, &block)
|
645
|
+
|
646
|
+
if line.kind_of? String
|
647
|
+
@cron_line = CronLine.new(line)
|
648
|
+
elsif line.kind_of? CronLine
|
649
|
+
@cron_line = line
|
650
|
+
else
|
651
|
+
raise \
|
652
|
+
"Cannot initialize a CronEntry " +
|
653
|
+
"with a param of class #{line.class}"
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
def matches? (time)
|
658
|
+
@cron_line.matches? time
|
659
|
+
end
|
660
|
+
|
661
|
+
def trigger
|
662
|
+
@block.call @eid, @cron_line
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
#
|
667
|
+
# A 'cron line' is a line in the sense of a crontab
|
668
|
+
# (man 5 crontab) file line.
|
669
|
+
#
|
670
|
+
class CronLine
|
671
|
+
|
672
|
+
attr_reader \
|
673
|
+
:minutes,
|
674
|
+
:hours,
|
675
|
+
:days,
|
676
|
+
:months,
|
677
|
+
:weekdays
|
678
|
+
|
679
|
+
def initialize (line)
|
680
|
+
|
681
|
+
super()
|
682
|
+
|
683
|
+
items = line.split
|
684
|
+
|
685
|
+
if items.length != 5
|
686
|
+
raise \
|
687
|
+
"cron '#{line}' string should hold 5 items, " +
|
688
|
+
"not #{items.length}" \
|
689
|
+
end
|
690
|
+
|
691
|
+
@minutes = parse_item(items[0], 0, 59)
|
692
|
+
@hours = parse_item(items[1], 0, 24)
|
693
|
+
@days = parse_item(items[2], 1, 31)
|
694
|
+
@months = parse_item(items[3], 1, 12)
|
695
|
+
@weekdays = parse_weekdays(items[4])
|
696
|
+
|
697
|
+
adjust_arrays()
|
698
|
+
end
|
699
|
+
|
700
|
+
def matches? (time)
|
701
|
+
|
702
|
+
if time.kind_of?(Float) or time.kind_of?(Integer)
|
703
|
+
time = Time.at(time)
|
704
|
+
end
|
705
|
+
|
706
|
+
return false if no_match?(time.min, @minutes)
|
707
|
+
return false if no_match?(time.hour, @hours)
|
708
|
+
return false if no_match?(time.day, @days)
|
709
|
+
return false if no_match?(time.month, @months)
|
710
|
+
return false if no_match?(time.wday, @weekdays)
|
711
|
+
|
712
|
+
return true
|
713
|
+
end
|
714
|
+
|
715
|
+
#
|
716
|
+
# Returns an array of 5 arrays (minutes, hours, days, months,
|
717
|
+
# weekdays).
|
718
|
+
# This method is used by the cronline unit tests.
|
719
|
+
#
|
720
|
+
def to_array
|
721
|
+
[ @minutes, @hours, @days, @months, @weekdays ]
|
722
|
+
end
|
723
|
+
|
724
|
+
private
|
725
|
+
|
726
|
+
#
|
727
|
+
# adjust values to Ruby
|
728
|
+
#
|
729
|
+
def adjust_arrays()
|
730
|
+
if @hours
|
731
|
+
@hours.each do |h|
|
732
|
+
h = 0 if h == 23
|
733
|
+
end
|
734
|
+
end
|
735
|
+
if @weekdays
|
736
|
+
@weekdays.each do |wd|
|
737
|
+
wd = wd - 1
|
738
|
+
end
|
739
|
+
end
|
740
|
+
end
|
741
|
+
|
742
|
+
WDS = [ "mon", "tue", "wed", "thu", "fri", "sat", "sun" ]
|
743
|
+
#
|
744
|
+
# used by parse_weekday()
|
745
|
+
|
746
|
+
def parse_weekdays (item)
|
747
|
+
|
748
|
+
item = item.downcase
|
749
|
+
|
750
|
+
WDS.each_with_index do |day, index|
|
751
|
+
item = item.gsub(day, "#{index+1}")
|
752
|
+
end
|
753
|
+
|
754
|
+
return parse_item(item, 1, 7)
|
755
|
+
end
|
756
|
+
|
757
|
+
def parse_item (item, min, max)
|
758
|
+
|
759
|
+
return nil \
|
760
|
+
if item == "*"
|
761
|
+
return parse_list(item, min, max) \
|
762
|
+
if item.index(",")
|
763
|
+
return parse_range(item, min, max) \
|
764
|
+
if item.index("*") or item.index("-")
|
765
|
+
|
766
|
+
i = Integer(item)
|
767
|
+
|
768
|
+
i = min if i < min
|
769
|
+
i = max if i > max
|
770
|
+
|
771
|
+
return [ i ]
|
772
|
+
end
|
773
|
+
|
774
|
+
def parse_list (item, min, max)
|
775
|
+
items = item.split(",")
|
776
|
+
result = []
|
777
|
+
items.each do |i|
|
778
|
+
i = Integer(i)
|
779
|
+
i = min if i < min
|
780
|
+
i = max if i > max
|
781
|
+
result << i
|
782
|
+
end
|
783
|
+
return result
|
784
|
+
end
|
785
|
+
|
786
|
+
def parse_range (item, min, max)
|
787
|
+
i = item.index("-")
|
788
|
+
j = item.index("/")
|
789
|
+
|
790
|
+
inc = 1
|
791
|
+
|
792
|
+
inc = Integer(item[j+1..-1]) if j
|
793
|
+
|
794
|
+
istart = -1
|
795
|
+
iend = -1
|
796
|
+
|
797
|
+
if i
|
798
|
+
|
799
|
+
istart = Integer(item[0..i-1])
|
800
|
+
|
801
|
+
if j
|
802
|
+
iend = Integer(item[i+1..j])
|
803
|
+
else
|
804
|
+
iend = Integer(item[i+1..-1])
|
805
|
+
end
|
806
|
+
|
807
|
+
else # case */x
|
808
|
+
istart = min
|
809
|
+
iend = max
|
810
|
+
end
|
811
|
+
|
812
|
+
istart = min if istart < min
|
813
|
+
iend = max if iend > max
|
814
|
+
|
815
|
+
result = []
|
816
|
+
|
817
|
+
value = istart
|
818
|
+
while true
|
819
|
+
result << value
|
820
|
+
value = value + inc
|
821
|
+
break if value > iend
|
822
|
+
end
|
823
|
+
|
824
|
+
return result
|
825
|
+
end
|
826
|
+
|
827
|
+
def no_match? (value, cron_values)
|
828
|
+
|
829
|
+
return false if not cron_values
|
830
|
+
|
831
|
+
cron_values.each do |v|
|
832
|
+
return false if value == v
|
833
|
+
end
|
834
|
+
|
835
|
+
return true
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
end
|
840
|
+
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: openwferu-scheduler
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.9.7
|
7
|
+
date: 2007-04-01 00:00:00 +09:00
|
8
|
+
summary: OpenWFEru scheduler for Ruby (at, cron and every)
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: john at openwfe dot org
|
12
|
+
homepage: http://openwferu.rubyforge.org/scheduler.html
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire: openwferu-scheduler
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: false
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- John Mettraux
|
31
|
+
files:
|
32
|
+
- lib/openwfe/util/otime.rb
|
33
|
+
- lib/openwfe/util/scheduler.rb
|
34
|
+
test_files: []
|
35
|
+
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
extra_rdoc_files: []
|
39
|
+
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
requirements: []
|
45
|
+
|
46
|
+
dependencies: []
|
47
|
+
|