rufus-scheduler 3.5.2 → 3.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
-