rufus-scheduler 1.0.7 → 1.0.8

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