rufus-scheduler 3.5.2 → 3.8.1

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,682 +0,0 @@
1
-
2
- module Rufus
3
-
4
- class Scheduler
5
-
6
- #--
7
- # job classes
8
- #++
9
-
10
- class Job
11
-
12
- #
13
- # Used by Job#kill
14
- #
15
- class KillSignal < StandardError; end
16
-
17
- attr_reader :id
18
- attr_reader :opts
19
- attr_reader :original
20
- attr_reader :scheduled_at
21
- attr_reader :last_time
22
- attr_reader :unscheduled_at
23
- attr_reader :tags
24
- attr_reader :count
25
- attr_reader :last_work_time
26
- attr_reader :mean_work_time
27
-
28
- # next trigger time
29
- #
30
- attr_accessor :next_time
31
-
32
- # previous "next trigger time"
33
- #
34
- attr_accessor :previous_time
35
-
36
- # anything with a #call(job[, timet]) method,
37
- # what gets actually triggered
38
- #
39
- attr_reader :callable
40
-
41
- # a reference to the instance whose call method is the @callable
42
- #
43
- attr_reader :handler
44
-
45
- def initialize(scheduler, original, opts, block)
46
-
47
- @scheduler = scheduler
48
- @original = original
49
- @opts = opts
50
-
51
- @handler = block
52
-
53
- @callable =
54
- if block.respond_to?(:arity)
55
- block
56
- elsif block.respond_to?(:call)
57
- block.method(:call)
58
- elsif block.is_a?(Class)
59
- @handler = block.new
60
- @handler.method(:call) rescue nil
61
- else
62
- nil
63
- end
64
-
65
- @scheduled_at = EoTime.now
66
- @unscheduled_at = nil
67
- @last_time = nil
68
-
69
- @locals = {}
70
- @local_mutex = Mutex.new
71
-
72
- @id = determine_id
73
-
74
- fail(
75
- ArgumentError,
76
- 'missing block or callable to schedule',
77
- caller[2..-1]
78
- ) unless @callable
79
-
80
- @tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
81
-
82
- @count = 0
83
- @last_work_time = 0.0
84
- @mean_work_time = 0.0
85
-
86
- # tidy up options
87
-
88
- if @opts[:allow_overlap] == false || @opts[:allow_overlapping] == false
89
- @opts[:overlap] = false
90
- end
91
- if m = @opts[:mutex]
92
- @opts[:mutex] = Array(m)
93
- end
94
- end
95
-
96
- alias job_id id
97
-
98
- # Will fail with an ArgumentError if the job frequency is higher than
99
- # the scheduler frequency.
100
- #
101
- def check_frequency
102
-
103
- # this parent implementation never fails
104
- end
105
-
106
- def trigger(time)
107
-
108
- @previous_time = @next_time
109
- set_next_time(time)
110
-
111
- do_trigger(time)
112
- end
113
-
114
- # Trigger the job right now, off of its schedule.
115
- #
116
- # Done in collaboration with Piavka in
117
- # https://github.com/jmettraux/rufus-scheduler/issues/214
118
- #
119
- def trigger_off_schedule(time=EoTime.now)
120
-
121
- do_trigger(time)
122
- end
123
-
124
- def unschedule
125
-
126
- @unscheduled_at = EoTime.now
127
- end
128
-
129
- def threads
130
-
131
- Thread.list.select { |t| t[:rufus_scheduler_job] == self }
132
- end
133
-
134
- # Kills all the threads this Job currently has going on.
135
- #
136
- def kill
137
-
138
- threads.each { |t| t.raise(KillSignal) }
139
- end
140
-
141
- def running?
142
-
143
- threads.any?
144
- end
145
-
146
- def scheduled?
147
-
148
- @scheduler.scheduled?(self)
149
- end
150
-
151
- def []=(key, value)
152
-
153
- @local_mutex.synchronize { @locals[key] = value }
154
- end
155
-
156
- def [](key)
157
-
158
- @local_mutex.synchronize { @locals[key] }
159
- end
160
-
161
- def key?(key)
162
-
163
- @local_mutex.synchronize { @locals.key?(key) }
164
- end
165
-
166
- def keys
167
-
168
- @local_mutex.synchronize { @locals.keys }
169
- end
170
-
171
- #def hash
172
- # self.object_id
173
- #end
174
- #def eql?(o)
175
- # o.class == self.class && o.hash == self.hash
176
- #end
177
- #
178
- # might be necessary at some point
179
-
180
- # Calls the callable (usually a block) wrapped in this Job instance.
181
- #
182
- # Warning: error rescueing is the responsibity of the caller.
183
- #
184
- def call(do_rescue=false)
185
-
186
- do_call(EoTime.now, do_rescue)
187
- end
188
-
189
- protected
190
-
191
- def callback(meth, time)
192
-
193
- return true unless @scheduler.respond_to?(meth)
194
-
195
- arity = @scheduler.method(meth).arity
196
- args = [ self, time ][0, (arity < 0 ? 2 : arity)]
197
-
198
- @scheduler.send(meth, *args)
199
- end
200
-
201
- def compute_timeout
202
-
203
- if to = @opts[:timeout]
204
- Rufus::Scheduler.parse(to)
205
- else
206
- nil
207
- end
208
- end
209
-
210
- def mutex(m)
211
-
212
- m.is_a?(Mutex) ? m : (@scheduler.mutexes[m.to_s] ||= Mutex.new)
213
- end
214
-
215
- def do_call(time, do_rescue)
216
-
217
- args = [ self, time ][0, @callable.arity]
218
- @callable.call(*args)
219
-
220
- rescue StandardError => se
221
-
222
- fail se unless do_rescue
223
-
224
- return if se.is_a?(KillSignal) # discard
225
-
226
- @scheduler.on_error(self, se)
227
-
228
- # exceptions above StandardError do pass through
229
- end
230
-
231
- def do_trigger(time)
232
-
233
- return if (
234
- opts[:overlap] == false &&
235
- running?
236
- )
237
- return if (
238
- callback(:confirm_lock, time) &&
239
- callback(:on_pre_trigger, time)
240
- ) == false
241
-
242
- @count += 1
243
-
244
- if opts[:blocking]
245
- trigger_now(time)
246
- else
247
- trigger_queue(time)
248
- end
249
- end
250
-
251
- def trigger_now(time)
252
-
253
- t = EoTime.now
254
- # if there are mutexes, t might be really bigger than time
255
-
256
- Thread.current[:rufus_scheduler_job] = self
257
- Thread.current[:rufus_scheduler_time] = t
258
- Thread.current[:rufus_scheduler_timeout] = compute_timeout
259
-
260
- @last_time = t
261
-
262
- do_call(time, true)
263
-
264
- ensure
265
-
266
- @last_work_time =
267
- EoTime.now - Thread.current[:rufus_scheduler_time]
268
- @mean_work_time =
269
- ((@count - 1) * @mean_work_time + @last_work_time) / @count
270
-
271
- post_trigger(time)
272
-
273
- Thread.current[:rufus_scheduler_job] = nil
274
- Thread.current[:rufus_scheduler_time] = nil
275
- Thread.current[:rufus_scheduler_timeout] = nil
276
- end
277
-
278
- def post_trigger(time)
279
-
280
- set_next_time(time, true)
281
-
282
- callback(:on_post_trigger, time)
283
- end
284
-
285
- def start_work_thread
286
-
287
- thread =
288
- Thread.new do
289
-
290
- Thread.current[:rufus_scheduler_job] = true
291
- # indicates that the thread is going to be assigned immediately
292
-
293
- Thread.current[@scheduler.thread_key] = true
294
- Thread.current[:rufus_scheduler_work_thread] = true
295
-
296
- loop do
297
-
298
- job, time = @scheduler.work_queue.pop
299
-
300
- break if @scheduler.started_at == nil
301
-
302
- next if job.unscheduled_at
303
-
304
- begin
305
-
306
- (job.opts[:mutex] || []).reduce(
307
- lambda { job.trigger_now(time) }
308
- ) do |b, m|
309
- lambda { mutex(m).synchronize { b.call } }
310
- end.call
311
-
312
- rescue KillSignal
313
-
314
- # simply go on looping
315
- end
316
- end
317
- end
318
-
319
- thread[@scheduler.thread_key] = true
320
- thread[:rufus_scheduler_work_thread] = true
321
- #
322
- # same as above (in the thead block),
323
- # but since it has to be done as quickly as possible.
324
- # So, whoever is running first (scheduler thread vs job thread)
325
- # sets this information
326
-
327
- thread
328
- end
329
-
330
- def trigger_queue(time)
331
-
332
- threads = @scheduler.work_threads
333
-
334
- vac = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
335
- que = @scheduler.work_queue.size
336
-
337
- cur = threads.size
338
- max = @scheduler.max_work_threads
339
-
340
- start_work_thread if vac - que < 1 && cur < max
341
-
342
- @scheduler.work_queue << [ self, time ]
343
- end
344
- end
345
-
346
- class OneTimeJob < Job
347
-
348
- alias time next_time
349
-
350
- def occurrences(time0, time1)
351
-
352
- (time >= time0 && time <= time1) ? [ time ] : []
353
- end
354
-
355
- protected
356
-
357
- def determine_id
358
-
359
- [
360
- self.class.name.split(':').last.downcase[0..-4],
361
- @scheduled_at.to_f,
362
- @next_time.to_f,
363
- (self.object_id < 0 ? 'm' : '') + self.object_id.to_s
364
- ].map(&:to_s).join('_')
365
- end
366
-
367
- # There is no next_time for one time jobs, hence the false.
368
- #
369
- def set_next_time(trigger_time, is_post=false)
370
-
371
- @next_time = is_post ? nil : false
372
- end
373
- end
374
-
375
- class AtJob < OneTimeJob
376
-
377
- def initialize(scheduler, time, opts, block)
378
-
379
- super(scheduler, time, opts, block)
380
-
381
- @next_time =
382
- opts[:_t] || Rufus::Scheduler.parse_at(time, opts)
383
- end
384
- end
385
-
386
- class InJob < OneTimeJob
387
-
388
- def initialize(scheduler, duration, opts, block)
389
-
390
- super(scheduler, duration, opts, block)
391
-
392
- @next_time =
393
- @scheduled_at +
394
- opts[:_t] || Rufus::Scheduler.parse_in(duration, opts)
395
- end
396
- end
397
-
398
- class RepeatJob < Job
399
-
400
- attr_reader :paused_at
401
-
402
- attr_reader :first_at
403
- attr_reader :last_at
404
- attr_accessor :times
405
-
406
- def initialize(scheduler, duration, opts, block)
407
-
408
- super
409
-
410
- @paused_at = nil
411
-
412
- @times = opts[:times]
413
-
414
- fail ArgumentError.new(
415
- "cannot accept :times => #{@times.inspect}, not nil or an int"
416
- ) unless @times == nil || @times.is_a?(Integer)
417
-
418
- self.first_at =
419
- opts[:first] || opts[:first_time] ||
420
- opts[:first_at] || opts[:first_in] ||
421
- nil
422
- self.last_at =
423
- opts[:last] || opts[:last_at] || opts[:last_in]
424
- end
425
-
426
- def first_at=(first)
427
-
428
- return (@first_at = nil) if first == nil
429
-
430
- n0 = EoTime.now
431
- n1 = n0 + 0.003
432
-
433
- first = n0 if first == :now || first == :immediately || first == 0
434
- fdur = Rufus::Scheduler.parse_duration(first, no_error: true)
435
-
436
- @first_at = (fdur && (EoTime.now + fdur)) || EoTime.make(first)
437
- @first_at = n1 if @first_at >= n0 && @first_at < n1
438
-
439
- fail ArgumentError.new(
440
- "cannot set first[_at|_in] in the past: " +
441
- "#{first.inspect} -> #{@first_at.inspect}"
442
- ) if @first_at < n0
443
-
444
- @first_at
445
- end
446
-
447
- def last_at=(last)
448
-
449
- @last_at =
450
- if last
451
- ldur = Rufus::Scheduler.parse_duration(last, no_error: true)
452
- (ldur && (EoTime.now + ldur)) || EoTime.make(last)
453
- else
454
- nil
455
- end
456
-
457
- fail ArgumentError.new(
458
- "cannot set last[_at|_in] in the past: " +
459
- "#{last.inspect} -> #{@last_at.inspect}"
460
- ) if last && @last_at < EoTime.now
461
-
462
- @last_at
463
- end
464
-
465
- def trigger(time)
466
-
467
- return if @paused_at
468
-
469
- return (@next_time = nil) if @times && @times < 1
470
- return (@next_time = nil) if @last_at && time >= @last_at
471
- #
472
- # It keeps jobs one step too much in @jobs, but it's OK
473
-
474
- super
475
-
476
- @times -= 1 if @times
477
- end
478
-
479
- def pause
480
-
481
- @paused_at = EoTime.now
482
- end
483
-
484
- def resume
485
-
486
- @paused_at = nil
487
- end
488
-
489
- def paused?
490
-
491
- @paused_at != nil
492
- end
493
-
494
- def determine_id
495
-
496
- [
497
- self.class.name.split(':').last.downcase[0..-4],
498
- @scheduled_at.to_f,
499
- (self.object_id < 0 ? 'm' : '') + self.object_id.to_s
500
- ].map(&:to_s).join('_')
501
- end
502
-
503
- def occurrences(time0, time1)
504
-
505
- a = []
506
-
507
- nt = @next_time
508
- ts = @times
509
-
510
- loop do
511
-
512
- break if nt > time1
513
- break if ts && ts <= 0
514
-
515
- a << nt if nt >= time0
516
-
517
- nt = next_time_from(nt)
518
- ts = ts - 1 if ts
519
- end
520
-
521
- a
522
- end
523
- end
524
-
525
- #
526
- # A parent class of EveryJob and IntervalJob
527
- #
528
- class EvInJob < RepeatJob
529
-
530
- def first_at=(first)
531
-
532
- @next_time = super
533
- end
534
- end
535
-
536
- class EveryJob < EvInJob
537
-
538
- attr_reader :frequency
539
-
540
- def initialize(scheduler, duration, opts, block)
541
-
542
- super(scheduler, duration, opts, block)
543
-
544
- @frequency = Rufus::Scheduler.parse_in(@original)
545
-
546
- fail ArgumentError.new(
547
- "cannot schedule #{self.class} with a frequency " +
548
- "of #{@frequency.inspect} (#{@original.inspect})"
549
- ) if @frequency <= 0
550
-
551
- set_next_time(nil)
552
- end
553
-
554
- def check_frequency
555
-
556
- fail ArgumentError.new(
557
- "job frequency (#{@frequency}s) is higher than " +
558
- "scheduler frequency (#{@scheduler.frequency}s)"
559
- ) if @frequency < @scheduler.frequency
560
- end
561
-
562
- protected
563
-
564
- def set_next_time(trigger_time, is_post=false)
565
-
566
- return if is_post
567
-
568
- n = EoTime.now
569
-
570
- @next_time =
571
- if @first_at && (trigger_time == nil || @first_at > n)
572
- @first_at
573
- else
574
- (@next_time || n) + @frequency
575
- end
576
- end
577
-
578
- def next_time_from(time)
579
-
580
- time + @frequency
581
- end
582
- end
583
-
584
- class IntervalJob < EvInJob
585
-
586
- attr_reader :interval
587
-
588
- def initialize(scheduler, interval, opts, block)
589
-
590
- super(scheduler, interval, opts, block)
591
-
592
- @interval = Rufus::Scheduler.parse_in(@original)
593
-
594
- fail ArgumentError.new(
595
- "cannot schedule #{self.class} with an interval " +
596
- "of #{@interval.inspect} (#{@original.inspect})"
597
- ) if @interval <= 0
598
-
599
- set_next_time(nil)
600
- end
601
-
602
- protected
603
-
604
- def set_next_time(trigger_time, is_post=false)
605
-
606
- @next_time =
607
- if is_post
608
- EoTime.now + @interval
609
- elsif trigger_time.nil?
610
- if @first_at == nil || @first_at < Time.now
611
- EoTime.now + @interval
612
- else
613
- @first_at
614
- end
615
- else
616
- false
617
- end
618
- end
619
-
620
- def next_time_from(time)
621
-
622
- time + @mean_work_time + @interval
623
- end
624
- end
625
-
626
- class CronJob < RepeatJob
627
-
628
- attr_reader :cron_line
629
-
630
- def initialize(scheduler, cronline, opts, block)
631
-
632
- super(scheduler, cronline, opts, block)
633
-
634
- @cron_line = opts[:_t] || ::Fugit::Cron.parse(cronline)
635
-
636
- set_next_time(nil)
637
- end
638
-
639
- def check_frequency
640
-
641
- return if @scheduler.frequency <= 1
642
- #
643
- # The minimum time delta in a cron job is 1 second, so if the
644
- # scheduler frequency is less than that, no worries.
645
-
646
- f = @cron_line.rough_frequency
647
-
648
- fail ArgumentError.new(
649
- "job frequency (min ~#{f}s) is higher than " +
650
- "scheduler frequency (#{@scheduler.frequency}s)"
651
- ) if f < @scheduler.frequency
652
- end
653
-
654
- def brute_frequency
655
-
656
- @cron_line.brute_frequency
657
- end
658
-
659
- def rough_frequency
660
-
661
- @cron_line.rough_frequency
662
- end
663
-
664
- protected
665
-
666
- def set_next_time(trigger_time, is_post=false)
667
-
668
- @next_time = next_time_from(trigger_time || Time.now)
669
- end
670
-
671
- def next_time_from(time)
672
-
673
- if @first_at == nil || @first_at <= time
674
- @cron_line.next_time(time)
675
- else
676
- @first_at
677
- end
678
- end
679
- end
680
- end
681
- end
682
-