rufus-scheduler 2.0.24 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/CHANGELOG.txt +6 -0
  2. data/CREDITS.txt +4 -0
  3. data/README.md +1064 -0
  4. data/Rakefile +1 -4
  5. data/TODO.txt +145 -55
  6. data/lib/rufus/scheduler.rb +502 -26
  7. data/lib/rufus/{sc → scheduler}/cronline.rb +46 -17
  8. data/lib/rufus/{sc/version.rb → scheduler/job_array.rb} +56 -4
  9. data/lib/rufus/scheduler/jobs.rb +548 -0
  10. data/lib/rufus/scheduler/util.rb +318 -0
  11. data/rufus-scheduler.gemspec +30 -4
  12. data/spec/cronline_spec.rb +29 -8
  13. data/spec/error_spec.rb +116 -0
  14. data/spec/job_array_spec.rb +39 -0
  15. data/spec/job_at_spec.rb +58 -0
  16. data/spec/job_cron_spec.rb +67 -0
  17. data/spec/job_every_spec.rb +71 -0
  18. data/spec/job_in_spec.rb +20 -0
  19. data/spec/job_interval_spec.rb +68 -0
  20. data/spec/job_repeat_spec.rb +308 -0
  21. data/spec/job_spec.rb +387 -115
  22. data/spec/lockfile_spec.rb +61 -0
  23. data/spec/parse_spec.rb +203 -0
  24. data/spec/schedule_at_spec.rb +129 -0
  25. data/spec/schedule_cron_spec.rb +66 -0
  26. data/spec/schedule_every_spec.rb +109 -0
  27. data/spec/schedule_in_spec.rb +80 -0
  28. data/spec/schedule_interval_spec.rb +128 -0
  29. data/spec/scheduler_spec.rb +831 -124
  30. data/spec/spec_helper.rb +65 -0
  31. data/spec/threads_spec.rb +75 -0
  32. metadata +64 -59
  33. data/README.rdoc +0 -661
  34. data/lib/rufus/otime.rb +0 -3
  35. data/lib/rufus/sc/jobqueues.rb +0 -160
  36. data/lib/rufus/sc/jobs.rb +0 -471
  37. data/lib/rufus/sc/rtime.rb +0 -363
  38. data/lib/rufus/sc/scheduler.rb +0 -636
  39. data/spec/at_in_spec.rb +0 -47
  40. data/spec/at_spec.rb +0 -125
  41. data/spec/blocking_spec.rb +0 -64
  42. data/spec/cron_spec.rb +0 -134
  43. data/spec/every_spec.rb +0 -304
  44. data/spec/exception_spec.rb +0 -113
  45. data/spec/in_spec.rb +0 -150
  46. data/spec/mutex_spec.rb +0 -159
  47. data/spec/rtime_spec.rb +0 -137
  48. data/spec/schedulable_spec.rb +0 -97
  49. data/spec/spec_base.rb +0 -87
  50. data/spec/stress_schedule_unschedule_spec.rb +0 -159
  51. data/spec/timeout_spec.rb +0 -148
  52. data/test/kjw.rb +0 -113
  53. data/test/t.rb +0 -20
@@ -22,10 +22,8 @@
22
22
  # Made in Japan.
23
23
  #++
24
24
 
25
- require 'tzinfo'
26
25
 
27
-
28
- module Rufus
26
+ class Rufus::Scheduler
29
27
 
30
28
  #
31
29
  # A 'cron line' is a line in the sense of a crontab
@@ -33,9 +31,6 @@ module Rufus
33
31
  #
34
32
  class CronLine
35
33
 
36
- DAY_S = 24 * 3600
37
- WEEK_S = 7 * DAY_S
38
-
39
34
  # The string used for creating this cronline instance.
40
35
  #
41
36
  attr_reader :original
@@ -51,7 +46,9 @@ module Rufus
51
46
 
52
47
  def initialize(line)
53
48
 
54
- super()
49
+ raise ArgumentError.new(
50
+ "not a string: #{line.inspect}"
51
+ ) unless line.is_a?(String)
55
52
 
56
53
  @original = line
57
54
 
@@ -123,12 +120,15 @@ module Rufus
123
120
  #
124
121
  # (Thanks to K Liu for the note and the examples)
125
122
  #
126
- def next_time(now=Time.now)
123
+ def next_time(from=Time.now)
124
+
125
+ time = @timezone ? @timezone.utc_to_local(from.getutc) : from
127
126
 
128
- time = @timezone ? @timezone.utc_to_local(now.getutc) : now
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)
129
129
 
130
- time = time - time.usec * 1e-6 + 1
131
- # small adjustment before starting
130
+ time = time + 1
131
+ # start at the next second
132
132
 
133
133
  loop do
134
134
 
@@ -150,7 +150,7 @@ module Rufus
150
150
 
151
151
  if @timezone
152
152
  time = @timezone.local_to_utc(time)
153
- time = time.getlocal unless now.utc?
153
+ time = time.getlocal unless from.utc?
154
154
  end
155
155
 
156
156
  time
@@ -159,19 +159,19 @@ module Rufus
159
159
  # Returns the previous the cronline matched. It's like next_time, but
160
160
  # for the past.
161
161
  #
162
- def previous_time(now=Time.now)
162
+ def previous_time(from=Time.now)
163
163
 
164
164
  # looks back by slices of two hours,
165
165
  #
166
166
  # finds for '* * * * sun', '* * 13 * *' and '0 12 13 * *'
167
167
  # starting 1970, 1, 1 in 1.8 to 2 seconds (says Rspec)
168
168
 
169
- start = current = now - 2 * 3600
169
+ start = current = from - 2 * 3600
170
170
  result = nil
171
171
 
172
172
  loop do
173
173
  nex = next_time(current)
174
- return (result ? result : previous_time(start)) if nex > now
174
+ return (result ? result : previous_time(start)) if nex > from
175
175
  result = current = nex
176
176
  end
177
177
 
@@ -196,9 +196,38 @@ module Rufus
196
196
  ]
197
197
  end
198
198
 
199
- private
199
+ # Returns the shortest delta between two potential occurences of the
200
+ # schedule described by this cronline.
201
+ #
202
+ def frequency
203
+
204
+ delta = 366 * DAY_S
205
+
206
+ t0 = previous_time(Time.local(2000, 1, 1))
207
+
208
+ loop do
209
+
210
+ break if delta <= 1
211
+ break if delta <= 60 && @seconds && @seconds.size == 1
212
+
213
+ t1 = next_time(t0)
214
+ d = t1 - t0
215
+ delta = d if d < delta
216
+
217
+ break if @months == nil && t1.month == 2
218
+ break if t1.year == 2001
219
+
220
+ t0 = t1
221
+ end
222
+
223
+ delta
224
+ end
225
+
226
+ protected
200
227
 
201
228
  WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
229
+ DAY_S = 24 * 3600
230
+ WEEK_S = 7 * DAY_S
202
231
 
203
232
  def parse_weekdays(item)
204
233
 
@@ -281,7 +310,7 @@ module Rufus
281
310
 
282
311
  raise ArgumentError.new(
283
312
  "#{item.inspect} is not in range #{min}..#{max}"
284
- ) if sta < min or edn > max
313
+ ) if sta < min || edn > max
285
314
 
286
315
  r = []
287
316
  val = sta
@@ -22,11 +22,63 @@
22
22
  # Made in Japan.
23
23
  #++
24
24
 
25
-
26
25
  module Rufus
27
- module Scheduler
28
26
 
29
- VERSION = '2.0.24'
30
- end
27
+ class Scheduler
28
+
29
+ #
30
+ # The array rufus-scheduler uses to keep jobs in order (next to trigger
31
+ # first).
32
+ #
33
+ class JobArray
34
+
35
+ def initialize
36
+
37
+ @mutex = Mutex.new
38
+ @array = []
39
+ end
40
+
41
+ def push(job)
42
+
43
+ @mutex.synchronize { @array << job unless @array.index(job) }
44
+
45
+ self
46
+ end
47
+
48
+ def size
49
+
50
+ @array.size
51
+ end
52
+
53
+ def each(now, &block)
54
+
55
+ to_a.sort_by { |j| j.next_time || (now + 1) }.each do |job|
56
+
57
+ break unless job.next_time
58
+ break if job.next_time > now
59
+
60
+ block.call(job)
61
+ end
62
+ end
63
+
64
+ def delete_unscheduled
65
+
66
+ @mutex.synchronize {
67
+
68
+ @array.delete_if { |j| j.next_time.nil? || j.unscheduled_at }
69
+ }
70
+ end
71
+
72
+ def to_a
73
+
74
+ @mutex.synchronize { @array.dup }
75
+ end
76
+
77
+ def [](job_id)
78
+
79
+ @mutex.synchronize { @array.find { |j| j.job_id == job_id } }
80
+ end
81
+ end
82
+ end
31
83
  end
32
84
 
@@ -0,0 +1,548 @@
1
+ #--
2
+ # Copyright (c) 2006-2013, 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
+
26
+ module Rufus
27
+
28
+ class Scheduler
29
+
30
+ #--
31
+ # job classes
32
+ #++
33
+
34
+ class Job
35
+
36
+ #
37
+ # Used by Job#kill
38
+ #
39
+ class KillSignal < StandardError; end
40
+
41
+ attr_reader :id
42
+ attr_reader :opts
43
+ attr_reader :original
44
+ attr_reader :scheduled_at
45
+ attr_reader :last_time
46
+ attr_reader :unscheduled_at
47
+ attr_reader :tags
48
+
49
+ # next trigger time
50
+ #
51
+ attr_accessor :next_time
52
+
53
+ # anything with a #call(job[, timet]) method,
54
+ # what gets actually triggered
55
+ #
56
+ attr_reader :callable
57
+
58
+ # a reference to the instance whose call method is the @callable
59
+ #
60
+ attr_reader :handler
61
+
62
+ def initialize(scheduler, original, opts, block)
63
+
64
+ @scheduler = scheduler
65
+ @original = original
66
+ @opts = opts
67
+
68
+ @handler = block
69
+
70
+ @callable =
71
+ if block.respond_to?(:arity)
72
+ block
73
+ elsif block.respond_to?(:call)
74
+ block.method(:call)
75
+ elsif block.is_a?(Class)
76
+ @handler = block.new
77
+ @handler.method(:call) rescue nil
78
+ else
79
+ nil
80
+ end
81
+
82
+ @scheduled_at = Time.now
83
+ @unscheduled_at = nil
84
+ @last_time = nil
85
+ #@mutexes = {}
86
+ #@pool_mutex = Mutex.new
87
+
88
+ @locals = {}
89
+ @local_mutex = Mutex.new
90
+
91
+ @id = determine_id
92
+
93
+ raise(
94
+ ArgumentError,
95
+ 'missing block or callable to schedule',
96
+ caller[2..-1]
97
+ ) unless @callable
98
+
99
+ @tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
100
+
101
+ # tidy up options
102
+
103
+ if @opts[:allow_overlap] == false || @opts[:allow_overlapping] == false
104
+ @opts[:overlap] = false
105
+ end
106
+ if m = @opts[:mutex]
107
+ @opts[:mutex] = Array(m)
108
+ end
109
+ end
110
+
111
+ alias job_id id
112
+
113
+ def trigger(time)
114
+
115
+ set_next_time(false, time)
116
+
117
+ return if opts[:overlap] == false && running?
118
+
119
+ r = callback(:pre, time)
120
+
121
+ return if r == false
122
+
123
+ if opts[:blocking]
124
+ do_trigger(time)
125
+ else
126
+ do_trigger_in_thread(time)
127
+ end
128
+ end
129
+
130
+ def unschedule
131
+
132
+ @unscheduled_at = Time.now
133
+ end
134
+
135
+ def threads
136
+
137
+ Thread.list.select { |t| t[:rufus_scheduler_job] == self }
138
+ end
139
+
140
+ # Kills all the threads this Job currently has going on.
141
+ #
142
+ def kill
143
+
144
+ threads.each { |t| t.raise(KillSignal) }
145
+ end
146
+
147
+ def running?
148
+
149
+ threads.any?
150
+ end
151
+
152
+ def scheduled?
153
+
154
+ @scheduler.scheduled?(self)
155
+ end
156
+
157
+ def []=(key, value)
158
+
159
+ @local_mutex.synchronize { @locals[key] = value }
160
+ end
161
+
162
+ def [](key)
163
+
164
+ @local_mutex.synchronize { @locals[key] }
165
+ end
166
+
167
+ def key?(key)
168
+
169
+ @local_mutex.synchronize { @locals.key?(key) }
170
+ end
171
+
172
+ def keys
173
+
174
+ @local_mutex.synchronize { @locals.keys }
175
+ end
176
+
177
+ #def hash
178
+ # self.object_id
179
+ #end
180
+ #def eql?(o)
181
+ # o.class == self.class && o.hash == self.hash
182
+ #end
183
+ #
184
+ # might be necessary at some point
185
+
186
+ protected
187
+
188
+ def callback(position, time)
189
+
190
+ name = position == :pre ? :on_pre_trigger : :on_post_trigger
191
+
192
+ return unless @scheduler.respond_to?(name)
193
+
194
+ args = @scheduler.method(name).arity < 2 ? [ self ] : [ self, time ]
195
+
196
+ @scheduler.send(name, *args)
197
+ end
198
+
199
+ def compute_timeout
200
+
201
+ if to = @opts[:timeout]
202
+ Rufus::Scheduler.parse(to)
203
+ else
204
+ nil
205
+ end
206
+ end
207
+
208
+ def mutex(m)
209
+
210
+ m.is_a?(Mutex) ? m : (@scheduler.mutexes[m.to_s] ||= Mutex.new)
211
+ end
212
+
213
+ def do_trigger(time)
214
+
215
+ t = Time.now
216
+ # if there are mutexes, t might be really bigger than time
217
+
218
+ Thread.current[:rufus_scheduler_job] = self
219
+ Thread.current[:rufus_scheduler_time] = t
220
+ Thread.current[:rufus_scheduler_timeout] = compute_timeout
221
+
222
+ @last_time = t
223
+
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)
234
+
235
+ ensure
236
+
237
+ post_trigger(time)
238
+
239
+ Thread.current[:rufus_scheduler_job] = nil
240
+ Thread.current[:rufus_scheduler_time] = nil
241
+ Thread.current[:rufus_scheduler_timeout] = nil
242
+ end
243
+
244
+ def post_trigger(time)
245
+
246
+ set_next_time(true, time)
247
+
248
+ callback(:post, time)
249
+ end
250
+
251
+ def start_work_thread
252
+
253
+ thread =
254
+ Thread.new do
255
+
256
+ Thread.current[@scheduler.thread_key] = true
257
+ Thread.current[:rufus_scheduler_job_thread] = true
258
+
259
+ loop do
260
+
261
+ job, time = @scheduler.work_queue.pop
262
+
263
+ break if @scheduler.started_at == nil
264
+
265
+ next if job.unscheduled_at
266
+
267
+ begin
268
+
269
+ (job.opts[:mutex] || []).reduce(
270
+ lambda { job.do_trigger(time) }
271
+ ) do |b, m|
272
+ lambda { mutex(m).synchronize { b.call } }
273
+ end.call
274
+
275
+ rescue KillSignal
276
+
277
+ # simply go on looping
278
+ end
279
+ end
280
+ end
281
+
282
+ thread[@scheduler.thread_key] = true
283
+ thread[:rufus_scheduler_work_thread] = true
284
+ #
285
+ # same as above (in the thead block),
286
+ # but since it has to be done as quickly as possible.
287
+ # So, whoever is running first (scheduler thread vs job thread)
288
+ # sets this information
289
+ end
290
+
291
+ def do_trigger_in_thread(time)
292
+
293
+ #@pool_mutex.synchronize do
294
+
295
+ count = @scheduler.work_threads.size
296
+ #vacant = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
297
+ #min = @scheduler.min_work_threads
298
+ max = @scheduler.max_work_threads
299
+
300
+ start_work_thread if count < max
301
+ #end
302
+
303
+ @scheduler.work_queue << [ self, time ]
304
+ end
305
+ end
306
+
307
+ class OneTimeJob < Job
308
+
309
+ alias time next_time
310
+
311
+ protected
312
+
313
+ def determine_id
314
+
315
+ [
316
+ self.class.name.split(':').last.downcase[0..-4],
317
+ @scheduled_at.to_f,
318
+ @next_time.to_f,
319
+ opts.hash.abs
320
+ ].map(&:to_s).join('_')
321
+ end
322
+
323
+ # There is no next_time for one time jobs, hence the false.
324
+ #
325
+ def set_next_time(is_post, trigger_time)
326
+
327
+ @next_time = is_post ? nil : false
328
+ end
329
+ end
330
+
331
+ class AtJob < OneTimeJob
332
+
333
+ def initialize(scheduler, time, opts, block)
334
+
335
+ super(scheduler, time, opts, block)
336
+
337
+ @next_time = Rufus::Scheduler.parse_at(time)
338
+ end
339
+ end
340
+
341
+ class InJob < OneTimeJob
342
+
343
+ def initialize(scheduler, duration, opts, block)
344
+
345
+ super(scheduler, duration, opts, block)
346
+
347
+ @next_time = @scheduled_at + Rufus::Scheduler.parse_in(duration)
348
+ end
349
+ end
350
+
351
+ class RepeatJob < Job
352
+
353
+ attr_reader :paused_at
354
+
355
+ attr_reader :first_at
356
+ attr_accessor :last_at
357
+ attr_accessor :times
358
+
359
+ def initialize(scheduler, duration, opts, block)
360
+
361
+ super
362
+
363
+ @paused_at = nil
364
+
365
+ @times = opts[:times]
366
+
367
+ raise ArgumentError.new(
368
+ "cannot accept :times => #{@times.inspect}, not nil or an int"
369
+ ) unless @times == nil || @times.is_a?(Fixnum)
370
+
371
+ self.first_at =
372
+ opts[:first] || opts[:first_at] || opts[:first_in] || 0
373
+ self.last_at =
374
+ opts[:last] || opts[:last_at] || opts[:last_in]
375
+ end
376
+
377
+ def first_at=(first)
378
+
379
+ @first_at = Rufus::Scheduler.parse_to_time(first)
380
+
381
+ raise ArgumentError.new(
382
+ "cannot set first[_at|_in] in the past: " +
383
+ "#{first.inspect} -> #{@first_at.inspect}"
384
+ ) if first != 0 && @first_at < Time.now
385
+ end
386
+
387
+ def last_at=(last)
388
+
389
+ @last_at = last ? Rufus::Scheduler.parse_to_time(last) : nil
390
+
391
+ raise ArgumentError.new(
392
+ "cannot set last[_at|_in] in the past: " +
393
+ "#{last.inspect} -> #{@last_at.inspect}"
394
+ ) if last && @last_at < Time.now
395
+ end
396
+
397
+ def trigger(time)
398
+
399
+ return if @paused_at
400
+ return if time < @first_at
401
+ #
402
+ # TODO: remove me when @first_at gets reworked
403
+
404
+ return (@next_time = nil) if @times && @times < 1
405
+ return (@next_time = nil) if @last_at && time >= @last_at
406
+ #
407
+ # TODO: rework that, jobs are thus kept 1 step too much in @jobs
408
+
409
+ super
410
+
411
+ @times = @times - 1 if @times
412
+ end
413
+
414
+ def pause
415
+
416
+ @paused_at = Time.now
417
+ end
418
+
419
+ def resume
420
+
421
+ @paused_at = nil
422
+ end
423
+
424
+ def paused?
425
+
426
+ @paused_at != nil
427
+ end
428
+
429
+ def determine_id
430
+
431
+ [
432
+ self.class.name.split(':').last.downcase[0..-4],
433
+ @scheduled_at.to_f,
434
+ opts.hash.abs
435
+ ].map(&:to_s).join('_')
436
+ end
437
+ end
438
+
439
+ #
440
+ # A parent class of EveryJob and IntervalJob
441
+ #
442
+ class EvInJob < RepeatJob
443
+
444
+ def first_at=(first)
445
+
446
+ super
447
+
448
+ @next_time = @first_at
449
+ end
450
+ end
451
+
452
+ class EveryJob < EvInJob
453
+
454
+ attr_reader :frequency
455
+
456
+ def initialize(scheduler, duration, opts, block)
457
+
458
+ super(scheduler, duration, opts, block)
459
+
460
+ @frequency = Rufus::Scheduler.parse_in(@original)
461
+
462
+ raise ArgumentError.new(
463
+ "cannot schedule #{self.class} with a frequency " +
464
+ "of #{@frequency.inspect} (#{@original.inspect})"
465
+ ) if @frequency <= 0
466
+
467
+ set_next_time(false, nil)
468
+ end
469
+
470
+ protected
471
+
472
+ def set_next_time(is_post, trigger_time)
473
+
474
+ return if is_post
475
+
476
+ @next_time =
477
+ if trigger_time
478
+ trigger_time + @frequency
479
+ elsif @first_at < Time.now
480
+ Time.now + @frequency
481
+ else
482
+ @first_at
483
+ end
484
+ end
485
+ end
486
+
487
+ class IntervalJob < EvInJob
488
+
489
+ attr_reader :interval
490
+
491
+ def initialize(scheduler, interval, opts, block)
492
+
493
+ super(scheduler, interval, opts, block)
494
+
495
+ @interval = Rufus::Scheduler.parse_in(@original)
496
+
497
+ raise ArgumentError.new(
498
+ "cannot schedule #{self.class} with an interval " +
499
+ "of #{@interval.inspect} (#{@original.inspect})"
500
+ ) if @interval <= 0
501
+
502
+ set_next_time(false, nil)
503
+ end
504
+
505
+ protected
506
+
507
+ def set_next_time(is_post, trigger_time)
508
+
509
+ @next_time =
510
+ if is_post
511
+ Time.now + @interval
512
+ elsif trigger_time.nil?
513
+ if @first_at < Time.now
514
+ Time.now + @interval
515
+ else
516
+ @first_at
517
+ end
518
+ else
519
+ false
520
+ end
521
+ end
522
+ end
523
+
524
+ class CronJob < RepeatJob
525
+
526
+ def initialize(scheduler, cronline, opts, block)
527
+
528
+ super(scheduler, cronline, opts, block)
529
+
530
+ @cron_line = CronLine.new(cronline)
531
+ @next_time = @cron_line.next_time
532
+ end
533
+
534
+ def frequency
535
+
536
+ @cron_line.frequency
537
+ end
538
+
539
+ protected
540
+
541
+ def set_next_time(is_post, trigger_time)
542
+
543
+ @next_time = @cron_line.next_time
544
+ end
545
+ end
546
+ end
547
+ end
548
+