rufus-scheduler 1.0.7 → 1.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.txt CHANGED
@@ -2,6 +2,17 @@
2
2
  = rufus-scheduler CHANGELOG.txt
3
3
 
4
4
 
5
+ == rufus-scheduler - 1.0.8 released 2008/07/18
6
+
7
+ - todo #21251 : added 'at', 'cron', 'in' and 'every' shortcut methods
8
+ - todo #21203 : keeping track of At and EveryJob until last trigger is over
9
+ - todo #20823 : now raising exception in case of bad 'at' parameter
10
+ - todo #21194 : added trigger_thread to Job class
11
+ - todo #21193 : spinned CronLine out to rufus/cronline.rb
12
+ - bug #20893 : sometimes unschedule(every_job) not working when job was
13
+ active (performing). Fixed.
14
+
15
+
5
16
  == rufus-scheduler - 1.0.7 released 2008/06/16
6
17
 
7
18
  - todo #20669 : implemented CronJob.next_time()
data/CREDITS.txt CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  == Feedback
5
5
 
6
+ - Sean Liu, unschedule_every issue
6
7
  - Manfred Usselman (no cronjobs on sundays bug)
7
8
  - Michael Goth, tests with precision > 1s
8
9
 
data/README.txt CHANGED
@@ -17,7 +17,35 @@ http://rubyforge.org/frs/?group_id=4812
17
17
 
18
18
  == usage
19
19
 
20
- For all the scheduling related information, see the Rufus::Scheduler class rdoc itself or the original OpenWFEru scheduler documentation at http://openwferu.rubyforge.org/scheduler.html
20
+ some examples :
21
+
22
+ require 'rubygems'
23
+ require 'rufus/scheduler'
24
+
25
+ scheduler = Rufus::Scheduler.start_new
26
+
27
+ scheduler.in("3d") do
28
+ regenerate_monthly_report()
29
+ end
30
+ #
31
+ # will call the regenerate_monthly_report method
32
+ # in 3 days from now
33
+
34
+ scheduler.every "10m10s" do
35
+ check_score(favourite_team) # every 10 minutes and 10 seconds
36
+ end
37
+
38
+ scheduler.cron "0 22 * * 1-5" do
39
+ log.info "activating security system..."
40
+ activate_security_system()
41
+ end
42
+
43
+ job_id = scheduler.at "Sun Oct 07 14:24:01 +0900 2009" do
44
+ init_self_destruction_sequence()
45
+ end
46
+
47
+
48
+ For all the scheduling related information, see the Rufus::Scheduler class rdoc itself (http://rufus.rubyforge.org/rufus-scheduler/classes/Rufus/Scheduler.html) or the original OpenWFEru scheduler documentation at http://openwferu.rubyforge.org/scheduler.html
21
49
 
22
50
  Apart from scheduling, There are also two interesting methods in this gem, they are named parse_time_string and to_time_string :
23
51
 
@@ -66,7 +94,7 @@ http://github.com/jmettraux/rufus-scheduler
66
94
 
67
95
  == author
68
96
 
69
- John Mettraux, jmettraux@gmail.com
97
+ John Mettraux, jmettraux@gmail.com
70
98
  http://jmettraux.wordpress.com
71
99
 
72
100
 
@@ -0,0 +1,304 @@
1
+ #
2
+ #--
3
+ # Copyright (c) 2006-2008, John Mettraux, jmettraux@gmail.com
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+ #++
23
+ #
24
+
25
+ #
26
+ # "made in Japan"
27
+ #
28
+ # John Mettraux at openwfe.org
29
+ #
30
+
31
+ module Rufus
32
+
33
+ #
34
+ # A 'cron line' is a line in the sense of a crontab
35
+ # (man 5 crontab) file line.
36
+ #
37
+ class CronLine
38
+
39
+ #
40
+ # The string used for creating this cronline instance.
41
+ #
42
+ attr_reader :original
43
+
44
+ attr_reader \
45
+ :seconds,
46
+ :minutes,
47
+ :hours,
48
+ :days,
49
+ :months,
50
+ :weekdays
51
+
52
+ def initialize (line)
53
+
54
+ super()
55
+
56
+ @original = line
57
+
58
+ items = line.split
59
+
60
+ unless [ 5, 6 ].include?(items.length)
61
+ raise \
62
+ "cron '#{line}' string should hold 5 or 6 items, " +
63
+ "not #{items.length}" \
64
+ end
65
+
66
+ offset = items.length - 5
67
+
68
+ @seconds = if offset == 1
69
+ parse_item(items[0], 0, 59)
70
+ else
71
+ [ 0 ]
72
+ end
73
+ @minutes = parse_item(items[0+offset], 0, 59)
74
+ @hours = parse_item(items[1+offset], 0, 24)
75
+ @days = parse_item(items[2+offset], 1, 31)
76
+ @months = parse_item(items[3+offset], 1, 12)
77
+ @weekdays = parse_weekdays(items[4+offset])
78
+
79
+ #adjust_arrays()
80
+ end
81
+
82
+ #
83
+ # Returns true if the given time matches this cron line.
84
+ #
85
+ # (the precision is passed as well to determine if it's
86
+ # worth checking seconds and minutes)
87
+ #
88
+ def matches? (time)
89
+ #def matches? (time, precision)
90
+
91
+ time = Time.at(time) unless time.kind_of?(Time)
92
+
93
+ return false \
94
+ if no_match?(time.sec, @seconds)
95
+ #if precision <= 1 and no_match?(time.sec, @seconds)
96
+ return false \
97
+ if no_match?(time.min, @minutes)
98
+ #if precision <= 60 and no_match?(time.min, @minutes)
99
+ return false \
100
+ if no_match?(time.hour, @hours)
101
+ return false \
102
+ if no_match?(time.day, @days)
103
+ return false \
104
+ if no_match?(time.month, @months)
105
+ return false \
106
+ if no_match?(time.wday, @weekdays)
107
+
108
+ true
109
+ end
110
+
111
+ #
112
+ # Returns an array of 6 arrays (seconds, minutes, hours, days,
113
+ # months, weekdays).
114
+ # This method is used by the cronline unit tests.
115
+ #
116
+ def to_array
117
+
118
+ [ @seconds, @minutes, @hours, @days, @months, @weekdays ]
119
+ end
120
+
121
+ #
122
+ # Returns the next time that this cron line is supposed to 'fire'
123
+ #
124
+ # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
125
+ #
126
+ def next_time (now = Time.now)
127
+
128
+ #
129
+ # position now to the next cron second
130
+
131
+ if @seconds
132
+ next_sec = @seconds.find { |s| s > now.sec } || 60 + @seconds.first
133
+ now += next_sec - now.sec
134
+ else
135
+ now += 1
136
+ end
137
+
138
+ #
139
+ # prepare sec jump array
140
+
141
+ sjarray = nil
142
+
143
+ if @seconds
144
+
145
+ sjarray = []
146
+
147
+ i = @seconds.index(now.sec)
148
+ ii = i
149
+
150
+ loop do
151
+ cur = @seconds[ii]
152
+ ii += 1
153
+ ii = 0 if ii == @seconds.size
154
+ nxt = @seconds[ii]
155
+ nxt += 60 if ii == 0
156
+ sjarray << (nxt - cur)
157
+ break if ii == i
158
+ end
159
+
160
+ else
161
+
162
+ sjarray = [ 1 ]
163
+ end
164
+
165
+ #
166
+ # ok, seek...
167
+
168
+ i = 0
169
+
170
+ loop do
171
+ return now if matches?(now)
172
+ now += sjarray[i]
173
+ i += 1
174
+ i = 0 if i == sjarray.size
175
+ # danger... potentially no exit...
176
+ end
177
+
178
+ nil
179
+ end
180
+
181
+ private
182
+
183
+ #--
184
+ # adjust values to Ruby
185
+ #
186
+ #def adjust_arrays()
187
+ # @hours = @hours.collect { |h|
188
+ # if h == 24
189
+ # 0
190
+ # else
191
+ # h
192
+ # end
193
+ # } if @hours
194
+ # @weekdays = @weekdays.collect { |wd|
195
+ # wd - 1
196
+ # } if @weekdays
197
+ #end
198
+ #
199
+ # dead code, keeping it as a reminder
200
+ #++
201
+
202
+ WDS = [ "sun", "mon", "tue", "wed", "thu", "fri", "sat" ]
203
+ #
204
+ # used by parse_weekday()
205
+
206
+ def parse_weekdays (item)
207
+
208
+ item = item.downcase
209
+
210
+ WDS.each_with_index do |day, index|
211
+ item = item.gsub day, "#{index}"
212
+ end
213
+
214
+ r = parse_item item, 0, 7
215
+
216
+ return r unless r.is_a?(Array)
217
+
218
+ r.collect { |e| e == 7 ? 0 : e }.uniq
219
+ end
220
+
221
+ def parse_item (item, min, max)
222
+
223
+ return nil \
224
+ if item == "*"
225
+ return parse_list(item, min, max) \
226
+ if item.index(",")
227
+ return parse_range(item, min, max) \
228
+ if item.index("*") or item.index("-")
229
+
230
+ i = Integer(item)
231
+
232
+ i = min if i < min
233
+ i = max if i > max
234
+
235
+ [ i ]
236
+ end
237
+
238
+ def parse_list (item, min, max)
239
+
240
+ items = item.split(",")
241
+
242
+ items.inject([]) { |r, i| r.push(parse_range(i, min, max)) }.flatten
243
+ end
244
+
245
+ def parse_range (item, min, max)
246
+
247
+ i = item.index("-")
248
+ j = item.index("/")
249
+
250
+ return item.to_i if (not i and not j)
251
+
252
+ inc = 1
253
+
254
+ inc = Integer(item[j+1..-1]) if j
255
+
256
+ istart = -1
257
+ iend = -1
258
+
259
+ if i
260
+
261
+ istart = Integer(item[0..i-1])
262
+
263
+ if j
264
+ iend = Integer(item[i+1..j])
265
+ else
266
+ iend = Integer(item[i+1..-1])
267
+ end
268
+
269
+ else # case */x
270
+
271
+ istart = min
272
+ iend = max
273
+ end
274
+
275
+ istart = min if istart < min
276
+ iend = max if iend > max
277
+
278
+ result = []
279
+
280
+ value = istart
281
+ loop do
282
+
283
+ result << value
284
+ value = value + inc
285
+ break if value > iend
286
+ end
287
+
288
+ result
289
+ end
290
+
291
+ def no_match? (value, cron_values)
292
+
293
+ return false if not cron_values
294
+
295
+ cron_values.each do |v|
296
+ return false if value == v # ok, it matches
297
+ end
298
+
299
+ true # no match found
300
+ end
301
+ end
302
+
303
+ end
304
+
data/lib/rufus/otime.rb CHANGED
@@ -336,6 +336,20 @@ module Rufus
336
336
  h
337
337
  end
338
338
 
339
+ #
340
+ # Ensures that a duration is a expressed as a Float instance.
341
+ #
342
+ # duration_to_f("10s")
343
+ #
344
+ # will yield 10.0
345
+ #
346
+ def Rufus.duration_to_f (s)
347
+
348
+ return s if s.kind_of?(Float)
349
+ return parse_time_string(s) if s.kind_of?(String)
350
+ Float(s.to_s)
351
+ end
352
+
339
353
  protected
340
354
 
341
355
  DURATIONS2M = [
@@ -31,7 +31,7 @@
31
31
  require 'thread'
32
32
  require 'monitor'
33
33
  require 'rufus/otime'
34
-
34
+ require 'rufus/cronline'
35
35
 
36
36
  module Rufus
37
37
 
@@ -45,7 +45,7 @@ module Rufus
45
45
  # params (usually an array or nil), either a block, which is more in the
46
46
  # Ruby way.
47
47
  #
48
- # == The gem "openwferu-scheduler"
48
+ # == The gem "rufus-scheduler"
49
49
  #
50
50
  # This scheduler was previously known as the "openwferu-scheduler" gem.
51
51
  #
@@ -114,6 +114,9 @@ module Rufus
114
114
  # regenerate_latest_report()
115
115
  # end
116
116
  #
117
+ # (note : a schedule every isn't triggered immediately, thus this example
118
+ # will first trigger 1 hour and 20 minutes after being scheduled)
119
+ #
117
120
  # The scheduler has a "exit_when_no_more_jobs" attribute. When set to
118
121
  # 'true', the scheduler will exit as soon as there are no more jobs to
119
122
  # run.
@@ -287,6 +290,20 @@ module Rufus
287
290
  #
288
291
  # scheduler.new :thread_name => "the crazy scheduler"
289
292
  #
293
+ #
294
+ # == job.trigger_thread
295
+ #
296
+ # Since rufus-scheduler 1.0.8, you can have access to the thread of
297
+ # a job currently being triggered.
298
+ #
299
+ # job = scheduler.get_job(job_id)
300
+ # thread = job.trigger_thread
301
+ #
302
+ # This new method will return nil if the job is not currently being
303
+ # triggered. Not that in case of an every or cron job, this method
304
+ # will return the thread of the last triggered instance, thus, in case
305
+ # of overlapping executions, you only get the most recent thread.
306
+ #
290
307
  class Scheduler
291
308
 
292
309
  #
@@ -324,6 +341,7 @@ module Rufus
324
341
 
325
342
  @pending_jobs = []
326
343
  @cron_jobs = {}
344
+ @non_cron_jobs = {}
327
345
 
328
346
  @schedule_queue = Queue.new
329
347
  @unschedule_queue = Queue.new
@@ -346,7 +364,7 @@ module Rufus
346
364
  #@correction = 0.00045
347
365
 
348
366
  @exit_when_no_more_jobs = false
349
- @dont_reschedule_every = false
367
+ #@dont_reschedule_every = false
350
368
 
351
369
  @last_cron_second = -1
352
370
 
@@ -467,6 +485,11 @@ module Rufus
467
485
  &block)
468
486
  end
469
487
 
488
+ #
489
+ # a shortcut for schedule_at
490
+ #
491
+ alias :at :schedule_at
492
+
470
493
 
471
494
  #
472
495
  # Schedules a job by stating in how much time it should trigger.
@@ -478,11 +501,16 @@ module Rufus
478
501
  def schedule_in (duration, params={}, &block)
479
502
 
480
503
  do_schedule_at(
481
- Time.new.to_f + duration_to_f(duration),
504
+ Time.new.to_f + Rufus::duration_to_f(duration),
482
505
  prepare_params(params),
483
506
  &block)
484
507
  end
485
508
 
509
+ #
510
+ # a shortcut for schedule_in
511
+ #
512
+ alias :in :schedule_in
513
+
486
514
  #
487
515
  # Schedules a job in a loop. After an execution, it will not execute
488
516
  # before the time specified in 'freq'.
@@ -505,76 +533,33 @@ module Rufus
505
533
  # # schedule something every two days, start in 5 hours...
506
534
  # end
507
535
  #
536
+ # (without setting a :first_in (or :first_at), our example schedule would
537
+ # have had been triggered after two days).
538
+ #
508
539
  def schedule_every (freq, params={}, &block)
509
540
 
510
- f = duration_to_f freq
511
-
512
541
  params = prepare_params params
513
- schedulable = params[:schedulable]
514
542
  params[:every] = freq
515
543
 
516
- first_at = params.delete :first_at
517
- first_in = params.delete :first_in
518
-
519
- previous_at = params[:previous_at]
544
+ first_at = params[:first_at]
545
+ first_in = params[:first_in]
520
546
 
521
- next_at = if first_at
522
- first_at
547
+ first_at = if first_at
548
+ at_to_f(first_at)
523
549
  elsif first_in
524
- Time.now.to_f + duration_to_f(first_in)
525
- elsif previous_at
526
- previous_at + f
550
+ Time.now.to_f + Rufus.duration_to_f(first_in)
527
551
  else
528
- Time.now.to_f + f
552
+ Time.now.to_f + Rufus.duration_to_f(freq) # not triggering immediately
529
553
  end
530
554
 
531
- do_schedule_at(next_at, params) do |job_id, at|
532
-
533
- #
534
- # trigger ...
535
-
536
- hit_exception = false
537
-
538
- begin
539
-
540
- if schedulable
541
- schedulable.trigger params
542
- else
543
- block.call job_id, at, params
544
- end
545
-
546
- rescue Exception => e
547
-
548
- log_exception e
549
-
550
- hit_exception = true
551
- end
552
-
553
- # cannot use a return here !!! (block)
554
-
555
- unless \
556
- @dont_reschedule_every or
557
- (params[:dont_reschedule] == true) or
558
- (hit_exception and params[:try_again] == false)
559
-
560
- #
561
- # ok, reschedule ...
562
-
563
- params[:job_id] = job_id
564
- params[:previous_at] = at
565
-
566
- schedule_every params[:every], params, &block
567
- #
568
- # yes, this is a kind of recursion
569
-
570
- # note that params[:every] might have been changed
571
- # by the block/schedulable code
572
- end
573
-
574
- job_id
575
- end
555
+ do_schedule_at(first_at, params, &block)
576
556
  end
577
557
 
558
+ #
559
+ # a shortcut for schedule_every
560
+ #
561
+ alias :every :schedule_every
562
+
578
563
  #
579
564
  # Schedules a cron job, the 'cron_line' is a string
580
565
  # following the Unix cron standard (see "man 5 crontab" in your command
@@ -607,11 +592,9 @@ module Rufus
607
592
  #
608
593
  # is a job with the same id already scheduled ?
609
594
 
610
- cron_id = params[:cron_id]
611
- cron_id = params[:job_id] unless cron_id
595
+ cron_id = params[:cron_id] || params[:job_id]
612
596
 
613
- #unschedule(cron_id) if cron_id
614
- @unschedule_queue << [ :cron, cron_id ]
597
+ #@unschedule_queue << cron_id
615
598
 
616
599
  #
617
600
  # schedule
@@ -619,12 +602,16 @@ module Rufus
619
602
  b = to_block(params, &block)
620
603
  job = CronJob.new(self, cron_id, cron_line, params, &b)
621
604
 
622
- #@cron_jobs[job.job_id] = job
623
605
  @schedule_queue << job
624
606
 
625
607
  job.job_id
626
608
  end
627
609
 
610
+ #
611
+ # an alias for schedule()
612
+ #
613
+ alias :cron :schedule
614
+
628
615
  #--
629
616
  #
630
617
  # The UNscheduling methods
@@ -637,15 +624,17 @@ module Rufus
637
624
  #
638
625
  def unschedule (job_id)
639
626
 
640
- @unschedule_queue << [ :at, job_id ]
627
+ @unschedule_queue << job_id
641
628
  end
642
629
 
643
630
  #
644
631
  # Unschedules a cron job
645
632
  #
633
+ # (deprecated : use unschedule(job_id) for all the jobs !)
634
+ #
646
635
  def unschedule_cron_job (job_id)
647
636
 
648
- @unschedule_queue << [ :cron, job_id ]
637
+ unschedule(job_id)
649
638
  end
650
639
 
651
640
  #--
@@ -660,7 +649,7 @@ module Rufus
660
649
  #
661
650
  def get_job (job_id)
662
651
 
663
- @cron_jobs[job_id] || @pending_jobs.find { |job| job.job_id == job_id }
652
+ @cron_jobs[job_id] || @non_cron_jobs[job_id]
664
653
  end
665
654
 
666
655
  #
@@ -669,13 +658,8 @@ module Rufus
669
658
  #
670
659
  def get_schedulable (job_id)
671
660
 
672
- #return nil unless job_id
673
-
674
661
  j = get_job(job_id)
675
-
676
- return j.schedulable if j.respond_to?(:schedulable)
677
-
678
- nil
662
+ j.respond_to?(:schedulable) ? j.schedulable : nil
679
663
  end
680
664
 
681
665
  #
@@ -684,7 +668,7 @@ module Rufus
684
668
  def find_jobs (tag)
685
669
 
686
670
  @cron_jobs.values.find_all { |job| job.has_tag?(tag) } +
687
- @pending_jobs.find_all { |job| job.has_tag?(tag) }
671
+ @non_cron_jobs.values.find_all { |job| job.has_tag?(tag) }
688
672
  end
689
673
 
690
674
  #
@@ -720,7 +704,7 @@ module Rufus
720
704
  #
721
705
  def every_job_count
722
706
 
723
- @pending_jobs.select { |j| j.is_a?(EveryJob) }.size
707
+ @non_cron_jobs.values.select { |j| j.class == EveryJob }.size
724
708
  end
725
709
 
726
710
  #
@@ -728,36 +712,43 @@ module Rufus
728
712
  #
729
713
  def at_job_count
730
714
 
731
- @pending_jobs.select { |j| j.instance_of?(AtJob) }.size
715
+ @non_cron_jobs.values.select { |j| j.class == AtJob }.size
732
716
  end
733
717
 
734
718
  #
735
719
  # Returns true if the given string seems to be a cron string.
736
720
  #
737
- def Scheduler.is_cron_string (s)
721
+ def self.is_cron_string (s)
738
722
 
739
723
  s.match ".+ .+ .+ .+ .+" # well...
740
724
  end
741
725
 
742
726
  private
743
727
 
728
+ #
729
+ # the unschedule work itself.
730
+ #
744
731
  def do_unschedule (job_id)
745
732
 
733
+ job = get_job job_id
734
+
735
+ return (@cron_jobs.delete(job_id) != nil) if job.is_a?(CronJob)
736
+
737
+ return false unless job # not found
738
+
739
+ if job.is_a?(AtJob) # catches AtJob and EveryJob instances
740
+ @non_cron_jobs.delete(job_id)
741
+ job.params[:dont_reschedule] = true # for AtJob as well, no worries
742
+ end
743
+
746
744
  for i in 0...@pending_jobs.length
747
745
  if @pending_jobs[i].job_id == job_id
748
746
  @pending_jobs.delete_at i
749
- return true
747
+ return true # asap
750
748
  end
751
749
  end
752
- #
753
- # not using delete_if because it scans the whole list
754
-
755
- do_unschedule_cron_job job_id
756
- end
757
750
 
758
- def do_unschedule_cron_job (job_id)
759
-
760
- (@cron_jobs.delete(job_id) != nil)
751
+ true
761
752
  end
762
753
 
763
754
  #
@@ -765,9 +756,7 @@ module Rufus
765
756
  #
766
757
  def prepare_params (params)
767
758
 
768
- params = { :schedulable => params } \
769
- if params.is_a?(Schedulable)
770
- params
759
+ params.is_a?(Schedulable) ? { :schedulable => params } : params
771
760
  end
772
761
 
773
762
  #
@@ -776,47 +765,33 @@ module Rufus
776
765
  #
777
766
  def do_schedule_at (at, params={}, &block)
778
767
 
779
- #puts "0 at is '#{at.to_s}' (#{at.class})"
768
+ job = params.delete :job
780
769
 
781
- at = at_to_f at
770
+ unless job
782
771
 
783
- #puts "1 at is '#{at.to_s}' (#{at.class})"}"
772
+ jobClass = params[:every] ? EveryJob : AtJob
784
773
 
785
- jobClass = params[:every] ? EveryJob : AtJob
774
+ b = to_block params, &block
786
775
 
787
- job_id = params[:job_id]
788
-
789
- b = to_block params, &block
776
+ job = jobClass.new self, at_to_f(at), params[:job_id], params, &b
777
+ end
790
778
 
791
- job = jobClass.new self, at, job_id, params, &b
779
+ if jobClass == AtJob && job.at < (Time.new.to_f + @precision)
792
780
 
793
- #do_unschedule(job_id) if job_id
781
+ job.trigger() unless params[:discard_past]
794
782
 
795
- if at < (Time.new.to_f + @precision)
783
+ @non_cron_jobs.delete job.job_id # just to be sure
796
784
 
797
- job.trigger() unless params[:discard_past]
798
785
  return nil
799
786
  end
800
787
 
788
+ @non_cron_jobs[job.job_id] = job
789
+
801
790
  @schedule_queue << job
802
791
 
803
792
  job.job_id
804
793
  end
805
794
 
806
- #
807
- # Ensures that a duration is a expressed as a Float instance.
808
- #
809
- # duration_to_f("10s")
810
- #
811
- # will yields 10.0
812
- #
813
- def duration_to_f (s)
814
-
815
- return s if s.kind_of?(Float)
816
- return Rufus::parse_time_string(s) if s.kind_of?(String)
817
- Float(s.to_s)
818
- end
819
-
820
795
  #
821
796
  # Ensures an 'at' instance is translated to a float
822
797
  # (to be compared with the float coming from time.to_f)
@@ -826,6 +801,9 @@ module Rufus
826
801
  at = Rufus::to_ruby_time(at) if at.kind_of?(String)
827
802
  at = Rufus::to_gm_time(at) if at.kind_of?(DateTime)
828
803
  at = at.to_f if at.kind_of?(Time)
804
+
805
+ raise "cannot schedule at : #{at.inspect}" unless at.is_a?(Float)
806
+
829
807
  at
830
808
  end
831
809
 
@@ -838,12 +816,10 @@ module Rufus
838
816
 
839
817
  return block if block
840
818
 
841
- schedulable = params[:schedulable]
819
+ schedulable = params.delete(:schedulable)
842
820
 
843
821
  return nil unless schedulable
844
822
 
845
- params.delete :schedulable
846
-
847
823
  l = lambda do
848
824
  schedulable.trigger(params)
849
825
  end
@@ -886,9 +862,6 @@ module Rufus
886
862
  #
887
863
  def step
888
864
 
889
- #puts Time.now.to_f
890
- #puts @pending_jobs.collect { |j| [ j.job_id, j.at ] }.inspect
891
-
892
865
  step_unschedule
893
866
  # unschedules any job in the unschedule queue before
894
867
  # they have a chance to get triggered.
@@ -911,15 +884,7 @@ module Rufus
911
884
 
912
885
  break if @unschedule_queue.empty?
913
886
 
914
- type, job_id = @unschedule_queue.pop
915
-
916
- if type == :cron
917
-
918
- do_unschedule_cron_job job_id
919
- else
920
-
921
- do_unschedule job_id
922
- end
887
+ do_unschedule(@unschedule_queue.pop)
923
888
  end
924
889
  end
925
890
 
@@ -947,22 +912,17 @@ module Rufus
947
912
  end
948
913
 
949
914
  #
950
- # triggers every eligible pending jobs, then every eligible
915
+ # triggers every eligible pending (at or every) jobs, then every eligible
951
916
  # cron jobs.
952
917
  #
953
918
  def step_trigger
954
919
 
955
- now = Time.new
956
-
957
- if @exit_when_no_more_jobs
958
-
959
- if @pending_jobs.size < 1
920
+ now = Time.now
960
921
 
961
- @stopped = true
962
- return
963
- end
922
+ if @exit_when_no_more_jobs && @pending_jobs.size < 1
964
923
 
965
- @dont_reschedule_every = true if at_job_count < 1
924
+ @stopped = true
925
+ return
966
926
  end
967
927
 
968
928
  # TODO : eventually consider running cron / pending
@@ -977,12 +937,9 @@ module Rufus
977
937
 
978
938
  @last_cron_second = now.sec
979
939
 
980
- #puts "step() @cron_jobs.size #{@cron_jobs.size}"
981
-
982
940
  @cron_jobs.each do |cron_id, cron_job|
983
- #puts "step() cron_id : #{cron_id}"
984
941
  #trigger(cron_job) if cron_job.matches?(now, @precision)
985
- trigger(cron_job) if cron_job.matches?(now)
942
+ cron_job.trigger if cron_job.matches?(now)
986
943
  end
987
944
  end
988
945
 
@@ -1005,29 +962,12 @@ module Rufus
1005
962
  #
1006
963
  # obviously
1007
964
 
1008
- trigger job
965
+ job.trigger
1009
966
 
1010
967
  @pending_jobs.delete_at 0
1011
968
  end
1012
969
  end
1013
970
 
1014
- #
1015
- # Triggers the job (in a dedicated thread).
1016
- #
1017
- def trigger (job)
1018
-
1019
- Thread.new do
1020
- begin
1021
-
1022
- job.trigger
1023
-
1024
- rescue Exception => e
1025
-
1026
- log_exception e
1027
- end
1028
- end
1029
- end
1030
-
1031
971
  #
1032
972
  # If an error occurs in the job, it well get caught and an error
1033
973
  # message will be displayed to STDOUT.
@@ -1108,6 +1048,12 @@ module Rufus
1108
1048
  #
1109
1049
  attr_reader :params
1110
1050
 
1051
+ #
1052
+ # if the job is currently executing, this field points to
1053
+ # the 'trigger thread'
1054
+ #
1055
+ attr_reader :trigger_thread
1056
+
1111
1057
 
1112
1058
  def initialize (scheduler, job_id, params, &block)
1113
1059
 
@@ -1147,6 +1093,31 @@ module Rufus
1147
1093
 
1148
1094
  @scheduler.unschedule(@job_id)
1149
1095
  end
1096
+
1097
+ #
1098
+ # Triggers the job (in a dedicated thread).
1099
+ #
1100
+ def trigger
1101
+
1102
+ Thread.new do
1103
+
1104
+ @trigger_thread = Thread.current
1105
+ # keeping track of the thread
1106
+
1107
+ begin
1108
+
1109
+ do_trigger
1110
+
1111
+ rescue Exception => e
1112
+
1113
+ @scheduler.send(:log_exception, e)
1114
+ end
1115
+
1116
+ #@trigger_thread = nil if @trigger_thread = Thread.current
1117
+ @trigger_thread = nil
1118
+ # overlapping executions, what to do ?
1119
+ end
1120
+ end
1150
1121
  end
1151
1122
 
1152
1123
  #
@@ -1160,23 +1131,13 @@ module Rufus
1160
1131
  #
1161
1132
  attr_accessor :at
1162
1133
 
1163
- #
1164
- # The constructor.
1165
- #
1134
+
1166
1135
  def initialize (scheduler, at, at_id, params, &block)
1167
1136
 
1168
1137
  super(scheduler, at_id, params, &block)
1169
1138
  @at = at
1170
1139
  end
1171
1140
 
1172
- #
1173
- # Triggers the job (calls the block)
1174
- #
1175
- def trigger
1176
-
1177
- @block.call @job_id, @at
1178
- end
1179
-
1180
1141
  #
1181
1142
  # Returns the Time instance at which this job is scheduled.
1182
1143
  #
@@ -1193,6 +1154,18 @@ module Rufus
1193
1154
 
1194
1155
  schedule_info
1195
1156
  end
1157
+
1158
+ protected
1159
+
1160
+ #
1161
+ # Triggers the job (calls the block)
1162
+ #
1163
+ def do_trigger
1164
+
1165
+ @block.call @job_id, @at
1166
+
1167
+ @scheduler.instance_variable_get(:@non_cron_jobs).delete @job_id
1168
+ end
1196
1169
  end
1197
1170
 
1198
1171
  #
@@ -1208,6 +1181,48 @@ module Rufus
1208
1181
 
1209
1182
  @params[:every]
1210
1183
  end
1184
+
1185
+ protected
1186
+
1187
+ #
1188
+ # triggers the job, then reschedules it if necessary
1189
+ #
1190
+ def do_trigger
1191
+
1192
+ hit_exception = false
1193
+
1194
+ begin
1195
+
1196
+ @block.call @job_id, @at, @params
1197
+
1198
+ rescue Exception => e
1199
+
1200
+ @scheduler.send(:log_exception, e)
1201
+
1202
+ hit_exception = true
1203
+ end
1204
+
1205
+ if \
1206
+ @scheduler.instance_variable_get(:@exit_when_no_more_jobs) or
1207
+ (@params[:dont_reschedule] == true) or
1208
+ (hit_exception and @params[:try_again] == false)
1209
+
1210
+ @scheduler.instance_variable_get(:@non_cron_jobs).delete(job_id)
1211
+ # maybe it'd be better to wipe that reference from here anyway...
1212
+
1213
+ return
1214
+ end
1215
+
1216
+ #
1217
+ # ok, reschedule ...
1218
+
1219
+
1220
+ params[:job] = self
1221
+
1222
+ @at = @at + Rufus.duration_to_f(params[:every])
1223
+
1224
+ @scheduler.send(:do_schedule_at, @at, params)
1225
+ end
1211
1226
  end
1212
1227
 
1213
1228
  #
@@ -1252,14 +1267,6 @@ module Rufus
1252
1267
  @cron_line.matches?(time)
1253
1268
  end
1254
1269
 
1255
- #
1256
- # As the name implies.
1257
- #
1258
- def trigger
1259
-
1260
- @block.call @job_id, @cron_line, @params
1261
- end
1262
-
1263
1270
  #
1264
1271
  # Returns the original cron tab string used to schedule this
1265
1272
  # Job. Like for example "60/3 * * * Sun".
@@ -1280,275 +1287,15 @@ module Rufus
1280
1287
 
1281
1288
  @cron_line.next_time(from)
1282
1289
  end
1283
- end
1284
-
1285
- #
1286
- # A 'cron line' is a line in the sense of a crontab
1287
- # (man 5 crontab) file line.
1288
- #
1289
- class CronLine
1290
-
1291
- #
1292
- # The string used for creating this cronline instance.
1293
- #
1294
- attr_reader :original
1295
-
1296
- attr_reader \
1297
- :seconds,
1298
- :minutes,
1299
- :hours,
1300
- :days,
1301
- :months,
1302
- :weekdays
1303
-
1304
- def initialize (line)
1305
-
1306
- super()
1307
-
1308
- @original = line
1309
-
1310
- items = line.split
1311
-
1312
- unless [ 5, 6 ].include?(items.length)
1313
- raise \
1314
- "cron '#{line}' string should hold 5 or 6 items, " +
1315
- "not #{items.length}" \
1316
- end
1317
-
1318
- offset = items.length - 5
1319
-
1320
- @seconds = if offset == 1
1321
- parse_item(items[0], 0, 59)
1322
- else
1323
- [ 0 ]
1324
- end
1325
- @minutes = parse_item(items[0+offset], 0, 59)
1326
- @hours = parse_item(items[1+offset], 0, 24)
1327
- @days = parse_item(items[2+offset], 1, 31)
1328
- @months = parse_item(items[3+offset], 1, 12)
1329
- @weekdays = parse_weekdays(items[4+offset])
1330
-
1331
- #adjust_arrays()
1332
- end
1333
-
1334
- #
1335
- # Returns true if the given time matches this cron line.
1336
- #
1337
- # (the precision is passed as well to determine if it's
1338
- # worth checking seconds and minutes)
1339
- #
1340
- def matches? (time)
1341
- #def matches? (time, precision)
1342
-
1343
- time = Time.at(time) unless time.kind_of?(Time)
1344
-
1345
- return false \
1346
- if no_match?(time.sec, @seconds)
1347
- #if precision <= 1 and no_match?(time.sec, @seconds)
1348
- return false \
1349
- if no_match?(time.min, @minutes)
1350
- #if precision <= 60 and no_match?(time.min, @minutes)
1351
- return false \
1352
- if no_match?(time.hour, @hours)
1353
- return false \
1354
- if no_match?(time.day, @days)
1355
- return false \
1356
- if no_match?(time.month, @months)
1357
- return false \
1358
- if no_match?(time.wday, @weekdays)
1359
-
1360
- true
1361
- end
1362
-
1363
- #
1364
- # Returns an array of 6 arrays (seconds, minutes, hours, days,
1365
- # months, weekdays).
1366
- # This method is used by the cronline unit tests.
1367
- #
1368
- def to_array
1369
-
1370
- [ @seconds, @minutes, @hours, @days, @months, @weekdays ]
1371
- end
1372
-
1373
- #
1374
- # Returns the next time that this cron line is supposed to 'fire'
1375
- #
1376
- # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
1377
- #
1378
- def next_time (now = Time.now)
1379
-
1380
- #
1381
- # position now to the next cron second
1382
1290
 
1383
- if @seconds
1384
- next_sec = @seconds.find { |s| s > now.sec } || 60 + @seconds.first
1385
- now += next_sec - now.sec
1386
- else
1387
- now += 1
1388
- end
1291
+ protected
1389
1292
 
1390
1293
  #
1391
- # prepare sec jump array
1392
-
1393
- sjarray = nil
1394
-
1395
- if @seconds
1396
-
1397
- sjarray = []
1398
-
1399
- i = @seconds.index(now.sec)
1400
- ii = i
1401
-
1402
- loop do
1403
- cur = @seconds[ii]
1404
- ii += 1
1405
- ii = 0 if ii == @seconds.size
1406
- nxt = @seconds[ii]
1407
- nxt += 60 if ii == 0
1408
- sjarray << (nxt - cur)
1409
- break if ii == i
1410
- end
1411
-
1412
- else
1413
-
1414
- sjarray = [ 1 ]
1415
- end
1416
-
1294
+ # As the name implies.
1417
1295
  #
1418
- # ok, seek...
1419
-
1420
- i = 0
1421
-
1422
- loop do
1423
- return now if matches?(now)
1424
- now += sjarray[i]
1425
- i += 1
1426
- i = 0 if i == sjarray.size
1427
- # danger... potentially no exit...
1428
- end
1429
-
1430
- nil
1431
- end
1432
-
1433
- private
1434
-
1435
- #--
1436
- # adjust values to Ruby
1437
- #
1438
- #def adjust_arrays()
1439
- # @hours = @hours.collect { |h|
1440
- # if h == 24
1441
- # 0
1442
- # else
1443
- # h
1444
- # end
1445
- # } if @hours
1446
- # @weekdays = @weekdays.collect { |wd|
1447
- # wd - 1
1448
- # } if @weekdays
1449
- #end
1450
- #
1451
- # dead code, keeping it as a reminder
1452
- #++
1453
-
1454
- WDS = [ "sun", "mon", "tue", "wed", "thu", "fri", "sat" ]
1455
- #
1456
- # used by parse_weekday()
1457
-
1458
- def parse_weekdays (item)
1459
-
1460
- item = item.downcase
1461
-
1462
- WDS.each_with_index do |day, index|
1463
- item = item.gsub day, "#{index}"
1464
- end
1465
-
1466
- r = parse_item item, 0, 7
1467
-
1468
- return r unless r.is_a?(Array)
1469
-
1470
- r.collect { |e| e == 7 ? 0 : e }.uniq
1471
- end
1472
-
1473
- def parse_item (item, min, max)
1474
-
1475
- return nil \
1476
- if item == "*"
1477
- return parse_list(item, min, max) \
1478
- if item.index(",")
1479
- return parse_range(item, min, max) \
1480
- if item.index("*") or item.index("-")
1481
-
1482
- i = Integer(item)
1483
-
1484
- i = min if i < min
1485
- i = max if i > max
1486
-
1487
- [ i ]
1488
- end
1489
-
1490
- def parse_list (item, min, max)
1491
-
1492
- items = item.split(",")
1493
-
1494
- items.inject([]) { |r, i| r.push(parse_range(i, min, max)) }.flatten
1495
- end
1496
-
1497
- def parse_range (item, min, max)
1498
-
1499
- i = item.index("-")
1500
- j = item.index("/")
1501
-
1502
- return item.to_i if (not i and not j)
1503
-
1504
- inc = 1
1505
-
1506
- inc = Integer(item[j+1..-1]) if j
1507
-
1508
- istart = -1
1509
- iend = -1
1510
-
1511
- if i
1512
-
1513
- istart = Integer(item[0..i-1])
1514
-
1515
- if j
1516
- iend = Integer(item[i+1..j])
1517
- else
1518
- iend = Integer(item[i+1..-1])
1519
- end
1520
-
1521
- else # case */x
1522
-
1523
- istart = min
1524
- iend = max
1525
- end
1526
-
1527
- istart = min if istart < min
1528
- iend = max if iend > max
1529
-
1530
- result = []
1531
-
1532
- value = istart
1533
- loop do
1534
-
1535
- result << value
1536
- value = value + inc
1537
- break if value > iend
1538
- end
1539
-
1540
- result
1541
- end
1542
-
1543
- def no_match? (value, cron_values)
1544
-
1545
- return false if not cron_values
1546
-
1547
- cron_values.each do |v|
1548
- return false if value == v # ok, it matches
1549
- end
1296
+ def do_trigger
1550
1297
 
1551
- true # no match found
1298
+ @block.call @job_id, @cron_line, @params
1552
1299
  end
1553
1300
  end
1554
1301