rufus-scheduler 3.4.2 → 3.7.0

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