rufus-scheduler 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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