rufus-scheduler 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.txt ADDED
@@ -0,0 +1,6 @@
1
+
2
+ = rufus-scheduler CHANGELOG.txt
3
+
4
+
5
+ == rufus-scheduler - 1.0 released 2008/01/24
6
+
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
+
@@ -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
+