rufus-scheduler 2.0.23 → 3.0.0

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.
Files changed (53) hide show
  1. data/CHANGELOG.txt +12 -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/{sc → scheduler}/cronline.rb +46 -17
  7. data/lib/rufus/{sc/version.rb → scheduler/job_array.rb} +56 -4
  8. data/lib/rufus/scheduler/jobs.rb +548 -0
  9. data/lib/rufus/scheduler/util.rb +318 -0
  10. data/lib/rufus/scheduler.rb +502 -26
  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 -65
  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
@@ -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
+