rufus-scheduler 1.0.5 → 1.0.6

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