rufus-scheduler 3.6.0 → 3.8.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,701 +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
- def next_times(count)
181
-
182
- next_time ? [ next_time ] : []
183
- end
184
-
185
- # Calls the callable (usually a block) wrapped in this Job instance.
186
- #
187
- # Warning: error rescueing is the responsibity of the caller.
188
- #
189
- def call(do_rescue=false)
190
-
191
- do_call(EoTime.now, do_rescue)
192
- end
193
-
194
- protected
195
-
196
- def callback(meth, time)
197
-
198
- return true unless @scheduler.respond_to?(meth)
199
-
200
- arity = @scheduler.method(meth).arity
201
- args = [ self, time ][0, (arity < 0 ? 2 : arity)]
202
-
203
- @scheduler.send(meth, *args)
204
- end
205
-
206
- def compute_timeout
207
-
208
- if to = @opts[:timeout]
209
- Rufus::Scheduler.parse(to)
210
- else
211
- nil
212
- end
213
- end
214
-
215
- def mutex(m)
216
-
217
- m.is_a?(Mutex) ? m : (@scheduler.mutexes[m.to_s] ||= Mutex.new)
218
- end
219
-
220
- def do_call(time, do_rescue)
221
-
222
- args = [ self, time ][0, @callable.arity]
223
- @callable.call(*args)
224
-
225
- rescue StandardError => se
226
-
227
- fail se unless do_rescue
228
-
229
- return if se.is_a?(KillSignal) # discard
230
-
231
- @scheduler.on_error(self, se)
232
-
233
- # exceptions above StandardError do pass through
234
- end
235
-
236
- def do_trigger(time)
237
-
238
- return if (
239
- opts[:overlap] == false &&
240
- running?
241
- )
242
- return if (
243
- callback(:confirm_lock, time) &&
244
- callback(:on_pre_trigger, time)
245
- ) == false
246
-
247
- @count += 1
248
-
249
- if opts[:blocking]
250
- trigger_now(time)
251
- else
252
- trigger_queue(time)
253
- end
254
- end
255
-
256
- def trigger_now(time)
257
-
258
- t = EoTime.now
259
- # if there are mutexes, t might be really bigger than time
260
-
261
- Thread.current[:rufus_scheduler_job] = self
262
- Thread.current[:rufus_scheduler_time] = t
263
- Thread.current[:rufus_scheduler_timeout] = compute_timeout
264
-
265
- @last_time = t
266
-
267
- do_call(time, true)
268
-
269
- ensure
270
-
271
- @last_work_time =
272
- EoTime.now - Thread.current[:rufus_scheduler_time]
273
- @mean_work_time =
274
- ((@count - 1) * @mean_work_time + @last_work_time) / @count
275
-
276
- post_trigger(time)
277
-
278
- Thread.current[:rufus_scheduler_job] = nil
279
- Thread.current[:rufus_scheduler_time] = nil
280
- Thread.current[:rufus_scheduler_timeout] = nil
281
- end
282
-
283
- def post_trigger(time)
284
-
285
- set_next_time(time, true)
286
-
287
- callback(:on_post_trigger, time)
288
- end
289
-
290
- def start_work_thread
291
-
292
- thread =
293
- Thread.new do
294
-
295
- Thread.current[:rufus_scheduler_job] = true
296
- # indicates that the thread is going to be assigned immediately
297
-
298
- Thread.current[@scheduler.thread_key] = true
299
- Thread.current[:rufus_scheduler_work_thread] = true
300
-
301
- loop do
302
-
303
- job, time = @scheduler.work_queue.pop
304
-
305
- break if @scheduler.started_at == nil
306
-
307
- next if job.unscheduled_at
308
-
309
- begin
310
-
311
- (job.opts[:mutex] || []).reduce(
312
- lambda { job.trigger_now(time) }
313
- ) do |b, m|
314
- lambda { mutex(m).synchronize { b.call } }
315
- end.call
316
-
317
- rescue KillSignal
318
-
319
- # simply go on looping
320
- end
321
- end
322
- end
323
-
324
- thread[@scheduler.thread_key] = true
325
- thread[:rufus_scheduler_work_thread] = true
326
- #
327
- # same as above (in the thead block),
328
- # but since it has to be done as quickly as possible.
329
- # So, whoever is running first (scheduler thread vs job thread)
330
- # sets this information
331
-
332
- thread
333
- end
334
-
335
- def trigger_queue(time)
336
-
337
- threads = @scheduler.work_threads
338
-
339
- vac = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
340
- que = @scheduler.work_queue.size
341
-
342
- cur = threads.size
343
- max = @scheduler.max_work_threads
344
-
345
- start_work_thread if vac - que < 1 && cur < max
346
-
347
- @scheduler.work_queue << [ self, time ]
348
- end
349
- end
350
-
351
- class OneTimeJob < Job
352
-
353
- alias time next_time
354
-
355
- def occurrences(time0, time1)
356
-
357
- (time >= time0 && time <= time1) ? [ time ] : []
358
- end
359
-
360
- protected
361
-
362
- def determine_id
363
-
364
- [
365
- self.class.name.split(':').last.downcase[0..-4],
366
- @scheduled_at.to_f,
367
- @next_time.to_f,
368
- (self.object_id < 0 ? 'm' : '') + self.object_id.to_s
369
- ].map(&:to_s).join('_')
370
- end
371
-
372
- # There is no next_time for one time jobs, hence the false.
373
- #
374
- def set_next_time(trigger_time, is_post=false)
375
-
376
- @next_time = is_post ? nil : false
377
- end
378
- end
379
-
380
- class AtJob < OneTimeJob
381
-
382
- def initialize(scheduler, time, opts, block)
383
-
384
- super(scheduler, time, opts, block)
385
-
386
- @next_time =
387
- opts[:_t] || Rufus::Scheduler.parse_at(time, opts)
388
- end
389
- end
390
-
391
- class InJob < OneTimeJob
392
-
393
- def initialize(scheduler, duration, opts, block)
394
-
395
- super(scheduler, duration, opts, block)
396
-
397
- @next_time =
398
- @scheduled_at +
399
- opts[:_t] || Rufus::Scheduler.parse_in(duration, opts)
400
- end
401
- end
402
-
403
- class RepeatJob < Job
404
-
405
- attr_reader :paused_at
406
-
407
- attr_reader :first_at
408
- attr_reader :last_at
409
- attr_accessor :times
410
-
411
- def initialize(scheduler, duration, opts, block)
412
-
413
- super
414
-
415
- @paused_at = nil
416
-
417
- @times = opts[:times]
418
-
419
- fail ArgumentError.new(
420
- "cannot accept :times => #{@times.inspect}, not nil or an int"
421
- ) unless @times == nil || @times.is_a?(Integer)
422
-
423
- self.first_at =
424
- opts[:first] || opts[:first_time] ||
425
- opts[:first_at] || opts[:first_in] ||
426
- nil
427
- self.last_at =
428
- opts[:last] || opts[:last_at] || opts[:last_in]
429
- end
430
-
431
- def first_at=(first)
432
-
433
- return (@first_at = nil) if first == nil
434
-
435
- n0 = EoTime.now
436
- n1 = n0 + 0.003
437
-
438
- first = n0 if first == :now || first == :immediately || first == 0
439
- fdur = Rufus::Scheduler.parse_duration(first, no_error: true)
440
-
441
- @first_at = (fdur && (EoTime.now + fdur)) || EoTime.make(first)
442
- @first_at = n1 if @first_at >= n0 && @first_at < n1
443
-
444
- fail ArgumentError.new(
445
- "cannot set first[_at|_in] in the past: " +
446
- "#{first.inspect} -> #{@first_at.inspect}"
447
- ) if @first_at < n0
448
-
449
- @first_at
450
- end
451
-
452
- def last_at=(last)
453
-
454
- @last_at =
455
- if last
456
- ldur = Rufus::Scheduler.parse_duration(last, no_error: true)
457
- (ldur && (EoTime.now + ldur)) || EoTime.make(last)
458
- else
459
- nil
460
- end
461
-
462
- fail ArgumentError.new(
463
- "cannot set last[_at|_in] in the past: " +
464
- "#{last.inspect} -> #{@last_at.inspect}"
465
- ) if last && @last_at < EoTime.now
466
-
467
- @last_at
468
- end
469
-
470
- def trigger(time)
471
-
472
- return if @paused_at
473
-
474
- return (@next_time = nil) if @times && @times < 1
475
- return (@next_time = nil) if @last_at && time >= @last_at
476
- #
477
- # It keeps jobs one step too much in @jobs, but it's OK
478
-
479
- super
480
-
481
- @times -= 1 if @times
482
- end
483
-
484
- def pause
485
-
486
- @paused_at = EoTime.now
487
- end
488
-
489
- def resume
490
-
491
- @paused_at = nil
492
- end
493
-
494
- def paused?
495
-
496
- @paused_at != nil
497
- end
498
-
499
- def determine_id
500
-
501
- [
502
- self.class.name.split(':').last.downcase[0..-4],
503
- @scheduled_at.to_f,
504
- (self.object_id < 0 ? 'm' : '') + self.object_id.to_s
505
- ].map(&:to_s).join('_')
506
- end
507
-
508
- def occurrences(time0, time1)
509
-
510
- a = []
511
-
512
- nt = @next_time
513
- ts = @times
514
-
515
- loop do
516
-
517
- break if nt > time1
518
- break if ts && ts <= 0
519
-
520
- a << nt if nt >= time0
521
-
522
- nt = next_time_from(nt)
523
- ts = ts - 1 if ts
524
- end
525
-
526
- a
527
- end
528
-
529
- # Starting from now, returns the {count} next occurences
530
- # (EtOrbi::EoTime instances) for this job.
531
- #
532
- # Warning, for IntervalJob, the @mean_work_time is used since
533
- # "interval" works from the end of a job to its next trigger
534
- # (not from one trigger to the next, as for "cron" and "every").
535
- #
536
- def next_times(count)
537
-
538
- (count - 1).times.inject([ next_time ]) { |a|
539
- a << next_time_from(a.last)
540
- a }
541
- end
542
- end
543
-
544
- #
545
- # A parent class of EveryJob and IntervalJob
546
- #
547
- class EvInJob < RepeatJob
548
-
549
- def first_at=(first)
550
-
551
- @next_time = super
552
- end
553
- end
554
-
555
- class EveryJob < EvInJob
556
-
557
- attr_reader :frequency
558
-
559
- def initialize(scheduler, duration, opts, block)
560
-
561
- super(scheduler, duration, opts, block)
562
-
563
- @frequency = Rufus::Scheduler.parse_in(@original)
564
-
565
- fail ArgumentError.new(
566
- "cannot schedule #{self.class} with a frequency " +
567
- "of #{@frequency.inspect} (#{@original.inspect})"
568
- ) if @frequency <= 0
569
-
570
- set_next_time(nil)
571
- end
572
-
573
- def check_frequency
574
-
575
- fail ArgumentError.new(
576
- "job frequency (#{@frequency}s) is higher than " +
577
- "scheduler frequency (#{@scheduler.frequency}s)"
578
- ) if @frequency < @scheduler.frequency
579
- end
580
-
581
- def next_time_from(time)
582
-
583
- time + @frequency
584
- end
585
-
586
- protected
587
-
588
- def set_next_time(trigger_time, is_post=false)
589
-
590
- return if is_post
591
-
592
- n = EoTime.now
593
-
594
- @next_time =
595
- if @first_at && (trigger_time == nil || @first_at > n)
596
- @first_at
597
- else
598
- (@next_time || n) + @frequency
599
- end
600
- end
601
- end
602
-
603
- class IntervalJob < EvInJob
604
-
605
- attr_reader :interval
606
-
607
- def initialize(scheduler, interval, opts, block)
608
-
609
- super(scheduler, interval, opts, block)
610
-
611
- @interval = Rufus::Scheduler.parse_in(@original)
612
-
613
- fail ArgumentError.new(
614
- "cannot schedule #{self.class} with an interval " +
615
- "of #{@interval.inspect} (#{@original.inspect})"
616
- ) if @interval <= 0
617
-
618
- set_next_time(nil)
619
- end
620
-
621
- def next_time_from(time)
622
-
623
- time + @mean_work_time + @interval
624
- end
625
-
626
- protected
627
-
628
- def set_next_time(trigger_time, is_post=false)
629
-
630
- @next_time =
631
- if is_post
632
- EoTime.now + @interval
633
- elsif trigger_time.nil?
634
- if @first_at == nil || @first_at < Time.now
635
- EoTime.now + @interval
636
- else
637
- @first_at
638
- end
639
- else
640
- false
641
- end
642
- end
643
- end
644
-
645
- class CronJob < RepeatJob
646
-
647
- attr_reader :cron_line
648
-
649
- def initialize(scheduler, cronline, opts, block)
650
-
651
- super(scheduler, cronline, opts, block)
652
-
653
- @cron_line = opts[:_t] || ::Fugit::Cron.do_parse(cronline)
654
-
655
- set_next_time(nil)
656
- end
657
-
658
- def check_frequency
659
-
660
- return if @scheduler.frequency <= 1
661
- #
662
- # The minimum time delta in a cron job is 1 second, so if the
663
- # scheduler frequency is less than that, no worries.
664
-
665
- f = @cron_line.rough_frequency
666
-
667
- fail ArgumentError.new(
668
- "job frequency (min ~#{f}s) is higher than " +
669
- "scheduler frequency (#{@scheduler.frequency}s)"
670
- ) if f < @scheduler.frequency
671
- end
672
-
673
- def brute_frequency
674
-
675
- @cron_line.brute_frequency
676
- end
677
-
678
- def rough_frequency
679
-
680
- @cron_line.rough_frequency
681
- end
682
-
683
- def next_time_from(time)
684
-
685
- if @first_at == nil || @first_at <= time
686
- @cron_line.next_time(time)
687
- else
688
- @first_at
689
- end
690
- end
691
-
692
- protected
693
-
694
- def set_next_time(trigger_time, is_post=false)
695
-
696
- @next_time = next_time_from(trigger_time || Time.now)
697
- end
698
- end
699
- end
700
- end
701
-