rufus-scheduler 3.5.1 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,646 +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[:rufus_scheduler_job] = true
283
- # indicates that the thread is going to be assigned immediately
284
-
285
- Thread.current[@scheduler.thread_key] = true
286
- Thread.current[:rufus_scheduler_work_thread] = true
287
-
288
- loop do
289
-
290
- job, time = @scheduler.work_queue.pop
291
-
292
- break if @scheduler.started_at == nil
293
-
294
- next if job.unscheduled_at
295
-
296
- begin
297
-
298
- (job.opts[:mutex] || []).reduce(
299
- lambda { job.trigger_now(time) }
300
- ) do |b, m|
301
- lambda { mutex(m).synchronize { b.call } }
302
- end.call
303
-
304
- rescue KillSignal
305
-
306
- # simply go on looping
307
- end
308
- end
309
- end
310
-
311
- thread[@scheduler.thread_key] = true
312
- thread[:rufus_scheduler_work_thread] = true
313
- #
314
- # same as above (in the thead block),
315
- # but since it has to be done as quickly as possible.
316
- # So, whoever is running first (scheduler thread vs job thread)
317
- # sets this information
318
-
319
- thread
320
- end
321
-
322
- def trigger_queue(time)
323
-
324
- threads = @scheduler.work_threads
325
-
326
- vac = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
327
- que = @scheduler.work_queue.size
328
-
329
- cur = threads.size
330
- max = @scheduler.max_work_threads
331
-
332
- start_work_thread if vac - que < 1 && cur < max
333
-
334
- @scheduler.work_queue << [ self, time ]
335
- end
336
- end
337
-
338
- class OneTimeJob < Job
339
-
340
- alias time next_time
341
-
342
- def occurrences(time0, time1)
343
-
344
- (time >= time0 && time <= time1) ? [ time ] : []
345
- end
346
-
347
- protected
348
-
349
- def determine_id
350
-
351
- [
352
- self.class.name.split(':').last.downcase[0..-4],
353
- @scheduled_at.to_f,
354
- @next_time.to_f,
355
- (self.object_id < 0 ? 'm' : '') + self.object_id.to_s
356
- ].map(&:to_s).join('_')
357
- end
358
-
359
- # There is no next_time for one time jobs, hence the false.
360
- #
361
- def set_next_time(trigger_time, is_post=false)
362
-
363
- @next_time = is_post ? nil : false
364
- end
365
- end
366
-
367
- class AtJob < OneTimeJob
368
-
369
- def initialize(scheduler, time, opts, block)
370
-
371
- super(scheduler, time, opts, block)
372
-
373
- @next_time =
374
- opts[:_t] || Rufus::Scheduler.parse_at(time, opts)
375
- end
376
- end
377
-
378
- class InJob < OneTimeJob
379
-
380
- def initialize(scheduler, duration, opts, block)
381
-
382
- super(scheduler, duration, opts, block)
383
-
384
- @next_time =
385
- @scheduled_at +
386
- opts[:_t] || Rufus::Scheduler.parse_in(duration, opts)
387
- end
388
- end
389
-
390
- class RepeatJob < Job
391
-
392
- attr_reader :paused_at
393
-
394
- attr_reader :first_at
395
- attr_reader :last_at
396
- attr_accessor :times
397
-
398
- def initialize(scheduler, duration, opts, block)
399
-
400
- super
401
-
402
- @paused_at = nil
403
-
404
- @times = opts[:times]
405
-
406
- fail ArgumentError.new(
407
- "cannot accept :times => #{@times.inspect}, not nil or an int"
408
- ) unless @times == nil || @times.is_a?(Integer)
409
-
410
- self.first_at =
411
- opts[:first] || opts[:first_time] ||
412
- opts[:first_at] || opts[:first_in] ||
413
- nil
414
- self.last_at =
415
- opts[:last] || opts[:last_at] || opts[:last_in]
416
- end
417
-
418
- def first_at=(first)
419
-
420
- return (@first_at = nil) if first == nil
421
-
422
- n0 = EoTime.now
423
- n1 = n0 + 0.003
424
-
425
- first = n0 if first == :now || first == :immediately || first == 0
426
- fdur = Rufus::Scheduler.parse_duration(first, no_error: true)
427
-
428
- @first_at = (fdur && (EoTime.now + fdur)) || EoTime.make(first)
429
- @first_at = n1 if @first_at >= n0 && @first_at < n1
430
-
431
- fail ArgumentError.new(
432
- "cannot set first[_at|_in] in the past: " +
433
- "#{first.inspect} -> #{@first_at.inspect}"
434
- ) if @first_at < n0
435
-
436
- @first_at
437
- end
438
-
439
- def last_at=(last)
440
-
441
- @last_at =
442
- if last
443
- ldur = Rufus::Scheduler.parse_duration(last, no_error: true)
444
- (ldur && (EoTime.now + ldur)) || EoTime.make(last)
445
- else
446
- nil
447
- end
448
-
449
- fail ArgumentError.new(
450
- "cannot set last[_at|_in] in the past: " +
451
- "#{last.inspect} -> #{@last_at.inspect}"
452
- ) if last && @last_at < EoTime.now
453
-
454
- @last_at
455
- end
456
-
457
- def trigger(time)
458
-
459
- return if @paused_at
460
-
461
- return (@next_time = nil) if @times && @times < 1
462
- return (@next_time = nil) if @last_at && time >= @last_at
463
- #
464
- # It keeps jobs one step too much in @jobs, but it's OK
465
-
466
- super
467
-
468
- @times -= 1 if @times
469
- end
470
-
471
- def pause
472
-
473
- @paused_at = EoTime.now
474
- end
475
-
476
- def resume
477
-
478
- @paused_at = nil
479
- end
480
-
481
- def paused?
482
-
483
- @paused_at != nil
484
- end
485
-
486
- def determine_id
487
-
488
- [
489
- self.class.name.split(':').last.downcase[0..-4],
490
- @scheduled_at.to_f,
491
- (self.object_id < 0 ? 'm' : '') + self.object_id.to_s
492
- ].map(&:to_s).join('_')
493
- end
494
-
495
- def occurrences(time0, time1)
496
-
497
- a = []
498
-
499
- nt = @next_time
500
- ts = @times
501
-
502
- loop do
503
-
504
- break if nt > time1
505
- break if ts && ts <= 0
506
-
507
- a << nt if nt >= time0
508
-
509
- nt = next_time_from(nt)
510
- ts = ts - 1 if ts
511
- end
512
-
513
- a
514
- end
515
- end
516
-
517
- #
518
- # A parent class of EveryJob and IntervalJob
519
- #
520
- class EvInJob < RepeatJob
521
-
522
- def first_at=(first)
523
-
524
- @next_time = super
525
- end
526
- end
527
-
528
- class EveryJob < EvInJob
529
-
530
- attr_reader :frequency
531
-
532
- def initialize(scheduler, duration, opts, block)
533
-
534
- super(scheduler, duration, opts, block)
535
-
536
- @frequency = Rufus::Scheduler.parse_in(@original)
537
-
538
- fail ArgumentError.new(
539
- "cannot schedule #{self.class} with a frequency " +
540
- "of #{@frequency.inspect} (#{@original.inspect})"
541
- ) if @frequency <= 0
542
-
543
- set_next_time(nil)
544
- end
545
-
546
- protected
547
-
548
- def set_next_time(trigger_time, is_post=false)
549
-
550
- return if is_post
551
-
552
- n = EoTime.now
553
-
554
- @next_time =
555
- if @first_at && (trigger_time == nil || @first_at > n)
556
- @first_at
557
- else
558
- (@next_time || n) + @frequency
559
- end
560
- end
561
-
562
- def next_time_from(time)
563
-
564
- time + @frequency
565
- end
566
- end
567
-
568
- class IntervalJob < EvInJob
569
-
570
- attr_reader :interval
571
-
572
- def initialize(scheduler, interval, opts, block)
573
-
574
- super(scheduler, interval, opts, block)
575
-
576
- @interval = Rufus::Scheduler.parse_in(@original)
577
-
578
- fail ArgumentError.new(
579
- "cannot schedule #{self.class} with an interval " +
580
- "of #{@interval.inspect} (#{@original.inspect})"
581
- ) if @interval <= 0
582
-
583
- set_next_time(nil)
584
- end
585
-
586
- protected
587
-
588
- def set_next_time(trigger_time, is_post=false)
589
-
590
- @next_time =
591
- if is_post
592
- EoTime.now + @interval
593
- elsif trigger_time.nil?
594
- if @first_at == nil || @first_at < Time.now
595
- EoTime.now + @interval
596
- else
597
- @first_at
598
- end
599
- else
600
- false
601
- end
602
- end
603
-
604
- def next_time_from(time)
605
-
606
- time + @mean_work_time + @interval
607
- end
608
- end
609
-
610
- class CronJob < RepeatJob
611
-
612
- attr_reader :cron_line
613
-
614
- def initialize(scheduler, cronline, opts, block)
615
-
616
- super(scheduler, cronline, opts, block)
617
-
618
- @cron_line = opts[:_t] || ::Fugit::Cron.parse(cronline)
619
-
620
- set_next_time(nil)
621
- end
622
-
623
- def brute_frequency
624
-
625
- @cron_line.brute_frequency
626
- end
627
-
628
- protected
629
-
630
- def set_next_time(trigger_time, is_post=false)
631
-
632
- @next_time = next_time_from(trigger_time || Time.now)
633
- end
634
-
635
- def next_time_from(time)
636
-
637
- if @first_at == nil || @first_at <= time
638
- @cron_line.next_time(time)
639
- else
640
- @first_at
641
- end
642
- end
643
- end
644
- end
645
- end
646
-