openwferu-scheduler 0.9.7
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/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
|
+
|