rufus-scheduler 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.txt 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
+