rufus-scheduler 3.0.0 → 3.0.9

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.
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2006-2014, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -106,15 +106,15 @@ class Rufus::Scheduler
106
106
  # be passed if no start time is specified (search start time set to
107
107
  # Time.now))
108
108
  #
109
- # Rufus::CronLine.new('30 7 * * *').next_time(
109
+ # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
110
110
  # Time.mktime(2008, 10, 24, 7, 29))
111
111
  # #=> Fri Oct 24 07:30:00 -0500 2008
112
112
  #
113
- # Rufus::CronLine.new('30 7 * * *').next_time(
113
+ # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
114
114
  # Time.utc(2008, 10, 24, 7, 29))
115
115
  # #=> Fri Oct 24 07:30:00 UTC 2008
116
116
  #
117
- # Rufus::CronLine.new('30 7 * * *').next_time(
117
+ # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
118
118
  # Time.utc(2008, 10, 24, 7, 29)).localtime
119
119
  # #=> Fri Oct 24 02:30:00 -0500 2008
120
120
  #
@@ -122,18 +122,18 @@ class Rufus::Scheduler
122
122
  #
123
123
  def next_time(from=Time.now)
124
124
 
125
- time = @timezone ? @timezone.utc_to_local(from.getutc) : from
126
-
127
- time = time.respond_to?(:round) ? time.round : time - time.usec * 1e-6
128
- # chop off subseconds (and yes, Ruby 1.8 doesn't have #round)
125
+ time = local_time(from)
126
+ time = round_to_seconds(time)
129
127
 
128
+ # start at the next second
130
129
  time = time + 1
131
- # start at the next second
132
130
 
133
131
  loop do
134
-
135
132
  unless date_match?(time)
136
- time += (24 - time.hour) * 3600 - time.min * 60 - time.sec; next
133
+ dst = time.isdst
134
+ time += (24 - time.hour) * 3600 - time.min * 60 - time.sec
135
+ time -= 3600 if time.isdst != dst # not necessary for winter, but...
136
+ next
137
137
  end
138
138
  unless sub_match?(time, :hour, @hours)
139
139
  time += (60 - time.min) * 60 - time.sec; next
@@ -148,34 +148,46 @@ class Rufus::Scheduler
148
148
  break
149
149
  end
150
150
 
151
- if @timezone
152
- time = @timezone.local_to_utc(time)
153
- time = time.getlocal unless from.utc?
154
- end
151
+ global_time(time, from.utc?)
155
152
 
156
- time
153
+ rescue TZInfo::PeriodNotFound
154
+
155
+ next_time(from + 3600)
157
156
  end
158
157
 
159
- # Returns the previous the cronline matched. It's like next_time, but
158
+ # Returns the previous time the cronline matched. It's like next_time, but
160
159
  # for the past.
161
160
  #
162
161
  def previous_time(from=Time.now)
163
162
 
164
- # looks back by slices of two hours,
165
- #
166
- # finds for '* * * * sun', '* * 13 * *' and '0 12 13 * *'
167
- # starting 1970, 1, 1 in 1.8 to 2 seconds (says Rspec)
163
+ time = local_time(from)
164
+ time = round_to_seconds(time)
168
165
 
169
- start = current = from - 2 * 3600
170
- result = nil
166
+ # start at the previous second
167
+ time = time - 1
171
168
 
172
169
  loop do
173
- nex = next_time(current)
174
- return (result ? result : previous_time(start)) if nex > from
175
- result = current = nex
170
+ unless date_match?(time)
171
+ time -= time.hour * 3600 + time.min * 60 + time.sec + 1; next
172
+ end
173
+ unless sub_match?(time, :hour, @hours)
174
+ time -= time.min * 60 + time.sec + 1; next
175
+ end
176
+ unless sub_match?(time, :min, @minutes)
177
+ time -= time.sec + 1; next
178
+ end
179
+ unless sub_match?(time, :sec, @seconds)
180
+ time -= 1; next
181
+ end
182
+
183
+ break
176
184
  end
177
185
 
178
- # never reached
186
+ global_time(time, from.utc?)
187
+
188
+ rescue TZInfo::PeriodNotFound
189
+
190
+ previous_time(time)
179
191
  end
180
192
 
181
193
  # Returns an array of 6 arrays (seconds, minutes, hours, days,
@@ -196,10 +208,53 @@ class Rufus::Scheduler
196
208
  ]
197
209
  end
198
210
 
211
+ # Returns a quickly computed approximation of the frequency for this
212
+ # cron line.
213
+ #
214
+ # #brute_frequency, on the other hand, will compute the frequency by
215
+ # examining a whole, that can take more than seconds for a seconds
216
+ # level cron...
217
+ #
218
+ def frequency
219
+
220
+ return brute_frequency unless @seconds && @seconds.length > 1
221
+
222
+ delta = 60
223
+ prev = @seconds[0]
224
+
225
+ @seconds[1..-1].each do |sec|
226
+ d = sec - prev
227
+ delta = d if d < delta
228
+ end
229
+
230
+ delta
231
+ end
232
+
199
233
  # Returns the shortest delta between two potential occurences of the
200
234
  # schedule described by this cronline.
201
235
  #
202
- def frequency
236
+ # .
237
+ #
238
+ # For a simple cronline like "*/5 * * * *", obviously the frequency is
239
+ # five minutes. Why does this method look at a whole year of #next_time ?
240
+ #
241
+ # Consider "* * * * sun#2,sun#3", the computed frequency is 1 week
242
+ # (the shortest delta is the one between the second sunday and the third
243
+ # sunday). This method takes no chance and runs next_time for the span
244
+ # of a whole year and keeps the shortest.
245
+ #
246
+ # Of course, this method can get VERY slow if you call on it a second-
247
+ # based cronline...
248
+ #
249
+ # Since it's a rarely used method, I haven't taken the time to make it
250
+ # smarter/faster.
251
+ #
252
+ # One obvious improvement would be to cache the result once computed...
253
+ #
254
+ # See https://github.com/jmettraux/rufus-scheduler/issues/89
255
+ # for a discussion about this method.
256
+ #
257
+ def brute_frequency
203
258
 
204
259
  delta = 366 * DAY_S
205
260
 
@@ -380,6 +435,32 @@ class Rufus::Scheduler
380
435
 
381
436
  [ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ]
382
437
  end
438
+
439
+ def local_time(time)
440
+
441
+ @timezone ? @timezone.utc_to_local(time.getutc) : time
442
+ end
443
+
444
+ def global_time(time, from_in_utc)
445
+
446
+ if @timezone
447
+ time =
448
+ begin
449
+ @timezone.local_to_utc(time)
450
+ rescue TZInfo::AmbiguousTime
451
+ @timezone.local_to_utc(time, time.isdst)
452
+ end
453
+ time = time.getlocal unless from_in_utc
454
+ end
455
+
456
+ time
457
+ end
458
+
459
+ def round_to_seconds(time)
460
+
461
+ # Ruby 1.8 doesn't have #round
462
+ time.respond_to?(:round) ? time.round : time - time.usec * 1e-6
463
+ end
383
464
  end
384
465
  end
385
466
 
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2006-2014, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -78,6 +78,13 @@ module Rufus
78
78
 
79
79
  @mutex.synchronize { @array.find { |j| j.job_id == job_id } }
80
80
  end
81
+
82
+ # Only used when shutting down, directly yields the underlying array.
83
+ #
84
+ def array
85
+
86
+ @array
87
+ end
81
88
  end
82
89
  end
83
90
  end
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2006-2014, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -45,6 +45,9 @@ module Rufus
45
45
  attr_reader :last_time
46
46
  attr_reader :unscheduled_at
47
47
  attr_reader :tags
48
+ attr_reader :count
49
+ attr_reader :last_work_time
50
+ attr_reader :mean_work_time
48
51
 
49
52
  # next trigger time
50
53
  #
@@ -82,8 +85,6 @@ module Rufus
82
85
  @scheduled_at = Time.now
83
86
  @unscheduled_at = nil
84
87
  @last_time = nil
85
- #@mutexes = {}
86
- #@pool_mutex = Mutex.new
87
88
 
88
89
  @locals = {}
89
90
  @local_mutex = Mutex.new
@@ -98,6 +99,10 @@ module Rufus
98
99
 
99
100
  @tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
100
101
 
102
+ @count = 0
103
+ @last_work_time = 0.0
104
+ @mean_work_time = 0.0
105
+
101
106
  # tidy up options
102
107
 
103
108
  if @opts[:allow_overlap] == false || @opts[:allow_overlapping] == false
@@ -112,13 +117,18 @@ module Rufus
112
117
 
113
118
  def trigger(time)
114
119
 
115
- set_next_time(false, time)
116
-
117
- return if opts[:overlap] == false && running?
120
+ set_next_time(time)
118
121
 
119
- r = callback(:pre, time)
122
+ return if (
123
+ opts[:overlap] == false &&
124
+ running?
125
+ )
126
+ return if (
127
+ callback(:confirm_lock, time) &&
128
+ callback(:on_pre_trigger, time)
129
+ ) == false
120
130
 
121
- return if r == false
131
+ @count += 1
122
132
 
123
133
  if opts[:blocking]
124
134
  do_trigger(time)
@@ -183,17 +193,25 @@ module Rufus
183
193
  #
184
194
  # might be necessary at some point
185
195
 
186
- protected
196
+ # Calls the callable (usually a block) wrapped in this Job instance.
197
+ #
198
+ # Warning: error rescueing is the responsibity of the caller.
199
+ #
200
+ def call(do_rescue=false)
201
+
202
+ do_call(Time.now, do_rescue)
203
+ end
187
204
 
188
- def callback(position, time)
205
+ protected
189
206
 
190
- name = position == :pre ? :on_pre_trigger : :on_post_trigger
207
+ def callback(meth, time)
191
208
 
192
- return unless @scheduler.respond_to?(name)
209
+ return true unless @scheduler.respond_to?(meth)
193
210
 
194
- args = @scheduler.method(name).arity < 2 ? [ self ] : [ self, time ]
211
+ arity = @scheduler.method(meth).arity
212
+ args = [ self, time ][0, (arity < 0 ? 2 : arity)]
195
213
 
196
- @scheduler.send(name, *args)
214
+ @scheduler.send(meth, *args)
197
215
  end
198
216
 
199
217
  def compute_timeout
@@ -210,6 +228,22 @@ module Rufus
210
228
  m.is_a?(Mutex) ? m : (@scheduler.mutexes[m.to_s] ||= Mutex.new)
211
229
  end
212
230
 
231
+ def do_call(time, do_rescue)
232
+
233
+ args = [ self, time ][0, @callable.arity]
234
+ @callable.call(*args)
235
+
236
+ rescue StandardError => se
237
+
238
+ raise se unless do_rescue
239
+
240
+ return if se.is_a?(KillSignal) # discard
241
+
242
+ @scheduler.on_error(self, se)
243
+
244
+ # exceptions above StandardError do pass through
245
+ end
246
+
213
247
  def do_trigger(time)
214
248
 
215
249
  t = Time.now
@@ -221,19 +255,15 @@ module Rufus
221
255
 
222
256
  @last_time = t
223
257
 
224
- args = [ self, time ][0, @callable.arity]
225
- @callable.call(*args)
226
-
227
- rescue KillSignal
228
-
229
- # discard
230
-
231
- rescue StandardError => se
232
-
233
- @scheduler.on_error(self, se)
258
+ do_call(time, true)
234
259
 
235
260
  ensure
236
261
 
262
+ @last_work_time =
263
+ Time.now - Thread.current[:rufus_scheduler_time]
264
+ @mean_work_time =
265
+ ((@count - 1) * @mean_work_time + @last_work_time) / @count
266
+
237
267
  post_trigger(time)
238
268
 
239
269
  Thread.current[:rufus_scheduler_job] = nil
@@ -243,9 +273,9 @@ module Rufus
243
273
 
244
274
  def post_trigger(time)
245
275
 
246
- set_next_time(true, time)
276
+ set_next_time(time, true)
247
277
 
248
- callback(:post, time)
278
+ callback(:on_post_trigger, time)
249
279
  end
250
280
 
251
281
  def start_work_thread
@@ -290,15 +320,15 @@ module Rufus
290
320
 
291
321
  def do_trigger_in_thread(time)
292
322
 
293
- #@pool_mutex.synchronize do
323
+ threads = @scheduler.work_threads
294
324
 
295
- count = @scheduler.work_threads.size
296
- #vacant = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
325
+ cur = threads.size
326
+ vac = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
297
327
  #min = @scheduler.min_work_threads
298
328
  max = @scheduler.max_work_threads
329
+ que = @scheduler.work_queue.size
299
330
 
300
- start_work_thread if count < max
301
- #end
331
+ start_work_thread if vac - que < 1 && cur < max
302
332
 
303
333
  @scheduler.work_queue << [ self, time ]
304
334
  end
@@ -308,6 +338,11 @@ module Rufus
308
338
 
309
339
  alias time next_time
310
340
 
341
+ def occurrences(time0, time1)
342
+
343
+ time >= time0 && time <= time1 ? [ time ] : []
344
+ end
345
+
311
346
  protected
312
347
 
313
348
  def determine_id
@@ -322,7 +357,7 @@ module Rufus
322
357
 
323
358
  # There is no next_time for one time jobs, hence the false.
324
359
  #
325
- def set_next_time(is_post, trigger_time)
360
+ def set_next_time(trigger_time, is_post=false)
326
361
 
327
362
  @next_time = is_post ? nil : false
328
363
  end
@@ -334,7 +369,8 @@ module Rufus
334
369
 
335
370
  super(scheduler, time, opts, block)
336
371
 
337
- @next_time = Rufus::Scheduler.parse_at(time)
372
+ @next_time =
373
+ opts[:_t] || Rufus::Scheduler.parse_at(time, opts)
338
374
  end
339
375
  end
340
376
 
@@ -344,7 +380,9 @@ module Rufus
344
380
 
345
381
  super(scheduler, duration, opts, block)
346
382
 
347
- @next_time = @scheduled_at + Rufus::Scheduler.parse_in(duration)
383
+ @next_time =
384
+ @scheduled_at +
385
+ opts[:_t] || Rufus::Scheduler.parse_in(duration, opts)
348
386
  end
349
387
  end
350
388
 
@@ -369,19 +407,24 @@ module Rufus
369
407
  ) unless @times == nil || @times.is_a?(Fixnum)
370
408
 
371
409
  self.first_at =
372
- opts[:first] || opts[:first_at] || opts[:first_in] || 0
410
+ opts[:first] || opts[:first_time] ||
411
+ opts[:first_at] || opts[:first_in] ||
412
+ 0
373
413
  self.last_at =
374
414
  opts[:last] || opts[:last_at] || opts[:last_in]
375
415
  end
376
416
 
377
417
  def first_at=(first)
378
418
 
419
+ n = Time.now
420
+ first = n + 0.001 if first == :now || first == :immediately
421
+
379
422
  @first_at = Rufus::Scheduler.parse_to_time(first)
380
423
 
381
424
  raise ArgumentError.new(
382
425
  "cannot set first[_at|_in] in the past: " +
383
426
  "#{first.inspect} -> #{@first_at.inspect}"
384
- ) if first != 0 && @first_at < Time.now
427
+ ) if first != 0 && @first_at < n
385
428
  end
386
429
 
387
430
  def last_at=(last)
@@ -408,7 +451,7 @@ module Rufus
408
451
 
409
452
  super
410
453
 
411
- @times = @times - 1 if @times
454
+ @times -= 1 if @times
412
455
  end
413
456
 
414
457
  def pause
@@ -434,6 +477,27 @@ module Rufus
434
477
  opts.hash.abs
435
478
  ].map(&:to_s).join('_')
436
479
  end
480
+
481
+ def occurrences(time0, time1)
482
+
483
+ a = []
484
+
485
+ nt = @next_time
486
+ ts = @times
487
+
488
+ loop do
489
+
490
+ break if nt > time1
491
+ break if ts && ts <= 0
492
+
493
+ a << nt if nt >= time0
494
+
495
+ nt = next_time_from(nt)
496
+ ts = ts - 1 if ts
497
+ end
498
+
499
+ a
500
+ end
437
501
  end
438
502
 
439
503
  #
@@ -464,12 +528,12 @@ module Rufus
464
528
  "of #{@frequency.inspect} (#{@original.inspect})"
465
529
  ) if @frequency <= 0
466
530
 
467
- set_next_time(false, nil)
531
+ set_next_time(nil)
468
532
  end
469
533
 
470
534
  protected
471
535
 
472
- def set_next_time(is_post, trigger_time)
536
+ def set_next_time(trigger_time, is_post=false)
473
537
 
474
538
  return if is_post
475
539
 
@@ -482,6 +546,11 @@ module Rufus
482
546
  @first_at
483
547
  end
484
548
  end
549
+
550
+ def next_time_from(time)
551
+
552
+ time + @frequency
553
+ end
485
554
  end
486
555
 
487
556
  class IntervalJob < EvInJob
@@ -499,12 +568,12 @@ module Rufus
499
568
  "of #{@interval.inspect} (#{@original.inspect})"
500
569
  ) if @interval <= 0
501
570
 
502
- set_next_time(false, nil)
571
+ set_next_time(nil)
503
572
  end
504
573
 
505
574
  protected
506
575
 
507
- def set_next_time(is_post, trigger_time)
576
+ def set_next_time(trigger_time, is_post=false)
508
577
 
509
578
  @next_time =
510
579
  if is_post
@@ -519,6 +588,11 @@ module Rufus
519
588
  false
520
589
  end
521
590
  end
591
+
592
+ def next_time_from(time)
593
+
594
+ time + @mean_work_time + @interval
595
+ end
522
596
  end
523
597
 
524
598
  class CronJob < RepeatJob
@@ -527,7 +601,7 @@ module Rufus
527
601
 
528
602
  super(scheduler, cronline, opts, block)
529
603
 
530
- @cron_line = CronLine.new(cronline)
604
+ @cron_line = opts[:_t] || CronLine.new(cronline)
531
605
  @next_time = @cron_line.next_time
532
606
  end
533
607
 
@@ -536,12 +610,22 @@ module Rufus
536
610
  @cron_line.frequency
537
611
  end
538
612
 
613
+ def brute_frequency
614
+
615
+ @cron_line.brute_frequency
616
+ end
617
+
539
618
  protected
540
619
 
541
- def set_next_time(is_post, trigger_time)
620
+ def set_next_time(trigger_time, is_post=false)
542
621
 
543
622
  @next_time = @cron_line.next_time
544
623
  end
624
+
625
+ def next_time_from(time)
626
+
627
+ @cron_line.next_time(time)
628
+ end
545
629
  end
546
630
  end
547
631
  end
@@ -0,0 +1,95 @@
1
+ #--
2
+ # Copyright (c) 2006-2014, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+ require 'fileutils'
26
+
27
+
28
+ class Rufus::Scheduler
29
+
30
+ #
31
+ # A lock that can always be acquired
32
+ #
33
+ class NullLock
34
+
35
+ # Locking is always successful.
36
+ #
37
+ def lock; true; end
38
+
39
+ def locked?; true; end
40
+ def unlock; true; end
41
+ end
42
+
43
+ #
44
+ # The standard flock mecha, with its own class thanks to @ecin
45
+ #
46
+ class FileLock
47
+
48
+ attr_reader :path
49
+
50
+ def initialize(path)
51
+
52
+ @path = path.to_s
53
+ end
54
+
55
+ # Locking is successful if this Ruby process can create and lock
56
+ # its lockfile (at the given path).
57
+ #
58
+ def lock
59
+
60
+ return true if locked?
61
+
62
+ @lockfile = nil
63
+
64
+ FileUtils.mkdir_p(::File.dirname(@path))
65
+
66
+ file = File.new(@path, File::RDWR | File::CREAT)
67
+ locked = file.flock(File::LOCK_NB | File::LOCK_EX)
68
+
69
+ return false unless locked
70
+
71
+ now = Time.now
72
+
73
+ file.print("pid: #{$$}, ")
74
+ file.print("scheduler.object_id: #{self.object_id}, ")
75
+ file.print("time: #{now}, ")
76
+ file.print("timestamp: #{now.to_f}")
77
+ file.flush
78
+
79
+ @lockfile = file
80
+
81
+ true
82
+ end
83
+
84
+ def unlock
85
+
86
+ !! (@lockfile && @lockfile.flock(File::LOCK_UN))
87
+ end
88
+
89
+ def locked?
90
+
91
+ !! (@lockfile && @lockfile.flock(File::LOCK_NB | File::LOCK_EX))
92
+ end
93
+ end
94
+ end
95
+