rufus-scheduler 1.0.12 → 1.0.13

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,1377 +1,3 @@
1
- #
2
- #--
3
- # Copyright (c) 2006-2008, John Mettraux, jmettraux@gmail.com
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the "Software"), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in
13
- # all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- # THE SOFTWARE.
22
- #++
23
- #
24
1
 
25
- #
26
- # "made in Japan"
27
- #
28
- # John Mettraux at openwfe.org
29
- #
30
-
31
- require 'thread'
32
- require 'rufus/otime'
33
- require 'rufus/cronline'
34
-
35
- module Rufus
36
-
37
- #
38
- # The Scheduler is used by OpenWFEru for registering 'at' and 'cron' jobs.
39
- # 'at' jobs to execute once at a given point in time. 'cron' jobs
40
- # execute a specified intervals.
41
- # The two main methods are thus schedule_at() and schedule().
42
- #
43
- # schedule_at() and schedule() await either a Schedulable instance and
44
- # params (usually an array or nil), either a block, which is more in the
45
- # Ruby way.
46
- #
47
- # == The gem "rufus-scheduler"
48
- #
49
- # This scheduler was previously known as the "openwferu-scheduler" gem.
50
- #
51
- # To ensure that code tapping the previous gem still runs fine with
52
- # "rufus-scheduler", this new gem has 'pointers' for the old class
53
- # names.
54
- #
55
- # require 'rubygems'
56
- # require 'openwfe/util/scheduler'
57
- # s = OpenWFE::Scheduler.new
58
- #
59
- # will still run OK with "rufus-scheduler".
60
- #
61
- # == Examples
62
- #
63
- # require 'rubygems'
64
- # require 'rufus/scheduler'
65
- #
66
- # scheduler = Rufus::Scheduler.start_new
67
- #
68
- # scheduler.schedule_in("3d") do
69
- # regenerate_monthly_report()
70
- # end
71
- # #
72
- # # will call the regenerate_monthly_report method
73
- # # in 3 days from now
74
- #
75
- # scheduler.schedule "0 22 * * 1-5" do
76
- # log.info "activating security system..."
77
- # activate_security_system()
78
- # end
79
- #
80
- # job_id = scheduler.schedule_at "Sun Oct 07 14:24:01 +0900 2009" do
81
- # init_self_destruction_sequence()
82
- # end
83
- #
84
- # scheduler.join # join the scheduler (prevents exiting)
85
- #
86
- #
87
- # an example that uses a Schedulable class :
88
- #
89
- # class Regenerator < Schedulable
90
- # def trigger (frequency)
91
- # self.send(frequency)
92
- # end
93
- # def monthly
94
- # # ...
95
- # end
96
- # def yearly
97
- # # ...
98
- # end
99
- # end
100
- #
101
- # regenerator = Regenerator.new
102
- #
103
- # scheduler.schedule_in("4d", regenerator)
104
- # #
105
- # # will regenerate the report in four days
106
- #
107
- # scheduler.schedule_in(
108
- # "5d",
109
- # { :schedulable => regenerator, :scope => :month })
110
- # #
111
- # # will regenerate the monthly report in 5 days
112
- #
113
- # There is also schedule_every() :
114
- #
115
- # scheduler.schedule_every("1h20m") do
116
- # regenerate_latest_report()
117
- # end
118
- #
119
- # (note : a schedule every isn't triggered immediately, thus this example
120
- # will first trigger 1 hour and 20 minutes after being scheduled)
121
- #
122
- # The scheduler has a "exit_when_no_more_jobs" attribute. When set to
123
- # 'true', the scheduler will exit as soon as there are no more jobs to
124
- # run.
125
- # Use with care though, if you create a scheduler, set this attribute
126
- # to true and start the scheduler, the scheduler will immediately exit.
127
- # This attribute is best used indirectly : the method
128
- # join_until_no_more_jobs() wraps it.
129
- #
130
- # The :scheduler_precision can be set when instantiating the scheduler.
131
- #
132
- # scheduler = Rufus::Scheduler.new(:scheduler_precision => 0.500)
133
- # scheduler.start
134
- # #
135
- # # instatiates a scheduler that checks its jobs twice per second
136
- # # (the default is 4 times per second (0.250))
137
- #
138
- # Note that rufus-scheduler places a constraint on the values for the
139
- # precision : 0.0 < p <= 1.0
140
- # Thus
141
- #
142
- # scheduler.precision = 4.0
143
- #
144
- # or
145
- #
146
- # scheduler = Rufus::Scheduler.new :scheduler_precision => 5.0
147
- #
148
- # will raise an exception.
149
- #
150
- #
151
- # == Tags
152
- #
153
- # Tags can be attached to jobs scheduled :
154
- #
155
- # scheduler.schedule_in "2h", :tags => "backup" do
156
- # init_backup_sequence()
157
- # end
158
- #
159
- # scheduler.schedule "0 24 * * *", :tags => "new_day" do
160
- # do_this_or_that()
161
- # end
162
- #
163
- # jobs = find_jobs 'backup'
164
- # jobs.each { |job| job.unschedule }
165
- #
166
- # Multiple tags may be attached to a single job :
167
- #
168
- # scheduler.schedule_in "2h", :tags => [ "backup", "important" ] do
169
- # init_backup_sequence()
170
- # end
171
- #
172
- # The vanilla case for tags assume they are String instances, but nothing
173
- # prevents you from using anything else. The scheduler has no persistence
174
- # by itself, so no serialization issue.
175
- #
176
- #
177
- # == Cron up to the second
178
- #
179
- # A cron schedule can be set at the second level :
180
- #
181
- # scheduler.schedule "7 * * * * *" do
182
- # puts "it's now the seventh second of the minute"
183
- # end
184
- #
185
- # The rufus scheduler recognizes an optional first column for second
186
- # scheduling. This column can, like for the other columns, specify a
187
- # value ("7"), a list of values ("7,8,9,27") or a range ("7-12").
188
- #
189
- #
190
- # == information passed to schedule blocks
191
- #
192
- # When calling schedule_every(), schedule_in() or schedule_at(), the block
193
- # expects zero or 3 parameters like in
194
- #
195
- # scheduler.schedule_every("1h20m") do |job_id, at, params|
196
- # puts "my job_id is #{job_id}"
197
- # end
198
- #
199
- # For schedule(), zero or two parameters can get passed
200
- #
201
- # scheduler.schedule "7 * * * * *" do |job_id, cron_line, params|
202
- # puts "my job_id is #{job_id}"
203
- # end
204
- #
205
- # In both cases, params corresponds to the params passed to the schedule
206
- # method (:tags, :first_at, :first_in, :dont_reschedule, ...)
207
- #
208
- #
209
- # == Exceptions
210
- #
211
- # The rufus scheduler will output a stacktrace to the STDOUT in
212
- # case of exception. There are two ways to change that behaviour.
213
- #
214
- # # 1 - providing a lwarn method to the scheduler instance :
215
- #
216
- # class << scheduler
217
- # def lwarn (&block)
218
- # puts "oops, something wrong happened : "
219
- # puts block.call
220
- # end
221
- # end
222
- #
223
- # # or
224
- #
225
- # def scheduler.lwarn (&block)
226
- # puts "oops, something wrong happened : "
227
- # puts block.call
228
- # end
229
- #
230
- # # 2 - overriding the [protected] method log_exception(e) :
231
- #
232
- # class << scheduler
233
- # def log_exception (e)
234
- # puts "something wrong happened : "+e.to_s
235
- # end
236
- # end
237
- #
238
- # # or
239
- #
240
- # def scheduler.log_exception (e)
241
- # puts "something wrong happened : "+e.to_s
242
- # end
243
- #
244
- # == 'Every jobs' and rescheduling
245
- #
246
- # Every jobs can reschedule/unschedule themselves. A reschedule example :
247
- #
248
- # schedule.schedule_every "5h" do |job_id, at, params|
249
- #
250
- # mails = $inbox.fetch_mails
251
- # mails.each { |m| $inbox.mark_as_spam(m) if is_spam(m) }
252
- #
253
- # params[:every] = if mails.size > 100
254
- # "1h" # lots of spam, check every hour
255
- # else
256
- # "5h" # normal schedule, every 5 hours
257
- # end
258
- # end
259
- #
260
- # Unschedule example :
261
- #
262
- # schedule.schedule_every "10s" do |job_id, at, params|
263
- # #
264
- # # polls every 10 seconds until a mail arrives
265
- #
266
- # $mail = $inbox.fetch_last_mail
267
- #
268
- # params[:dont_reschedule] = true if $mail
269
- # end
270
- #
271
- # == 'Every jobs', :first_at and :first_in
272
- #
273
- # Since rufus-scheduler 1.0.2, the schedule_every methods recognizes two
274
- # optional parameters, :first_at and :first_in
275
- #
276
- # scheduler.schedule_every "2d", :first_in => "5h" do
277
- # # schedule something every two days, start in 5 hours...
278
- # end
279
- #
280
- # scheduler.schedule_every "2d", :first_at => "5h" do
281
- # # schedule something every two days, start in 5 hours...
282
- # end
283
- #
284
- # == job.next_time()
285
- #
286
- # Jobs, be they at, every or cron have a next_time() method, which tells
287
- # when the job will be fired next time (for at and in jobs, this is also the
288
- # last time).
289
- #
290
- # For cron jobs, the current implementation is quite brutal. It takes three
291
- # seconds on my 2006 macbook to reach a cron schedule 1 year away.
292
- #
293
- # When is the next friday 13th ?
294
- #
295
- # require 'rubygems'
296
- # require 'rufus/scheduler'
297
- #
298
- # puts Rufus::CronLine.new("* * 13 * fri").next_time
299
- #
300
- #
301
- # == :thread_name option
302
- #
303
- # You can specify the name of the scheduler's thread. Should make
304
- # it easier in some debugging situations.
305
- #
306
- # scheduler.new :thread_name => "the crazy scheduler"
307
- #
308
- #
309
- # == job.trigger_thread
310
- #
311
- # Since rufus-scheduler 1.0.8, you can have access to the thread of
312
- # a job currently being triggered.
313
- #
314
- # job = scheduler.get_job(job_id)
315
- # thread = job.trigger_thread
316
- #
317
- # This new method will return nil if the job is not currently being
318
- # triggered. Not that in case of an every or cron job, this method
319
- # will return the thread of the last triggered instance, thus, in case
320
- # of overlapping executions, you only get the most recent thread.
321
- #
322
- #
323
- # == specifying a :timeout for a job
324
- #
325
- # rufus-scheduler 1.0.12 introduces a :timeout parameter for jobs.
326
- #
327
- # scheduler.every "3h", :timeout => '2h30m' do
328
- # do_that_long_job()
329
- # end
330
- #
331
- # after 2 hours and half, the 'long job' will get interrupted by a
332
- # Rufus::TimeOutError (so that you know what to catch).
333
- #
334
- # :timeout is applicable to all types of jobs : at, in, every, cron. It
335
- # accepts a String value following the "Mdhms" scheme the rufus-scheduler
336
- # uses.
337
- #
338
- class Scheduler
339
-
340
- VERSION = '1.0.12'
341
-
342
- #
343
- # By default, the precision is 0.250, with means the scheduler
344
- # will check for jobs to execute 4 times per second.
345
- #
346
- attr_reader :precision
347
-
348
- #
349
- # Setting the precision ( 0.0 < p <= 1.0 )
350
- #
351
- def precision= (f)
352
-
353
- raise 'precision must be 0.0 < p <= 1.0' \
354
- if f <= 0.0 or f > 1.0
355
-
356
- @precision = f
357
- end
358
-
359
- #--
360
- # Set by default at 0.00045, it's meant to minimize drift
361
- #
362
- #attr_accessor :correction
363
- #++
364
-
365
- #
366
- # As its name implies.
367
- #
368
- attr_accessor :stopped
369
-
370
-
371
- def initialize (params={})
372
-
373
- super()
374
-
375
- @pending_jobs = []
376
- @cron_jobs = {}
377
- @non_cron_jobs = {}
378
-
379
- @schedule_queue = Queue.new
380
- @unschedule_queue = Queue.new
381
- #
382
- # sync between the step() method and the [un]schedule
383
- # methods is done via these queues, no more mutex
384
-
385
- @scheduler_thread = nil
386
-
387
- @precision = 0.250
388
- # every 250ms, the scheduler wakes up (default value)
389
- begin
390
- self.precision = Float(params[:scheduler_precision])
391
- rescue Exception => e
392
- # let precision at its default value
393
- end
394
-
395
- @thread_name = params[:thread_name] || "rufus scheduler"
396
-
397
- #@correction = 0.00045
398
-
399
- @exit_when_no_more_jobs = false
400
- #@dont_reschedule_every = false
401
-
402
- @last_cron_second = -1
403
-
404
- @stopped = true
405
- end
406
-
407
- #
408
- # Starts this scheduler (or restart it if it was previously stopped)
409
- #
410
- def start
411
-
412
- @stopped = false
413
-
414
- @scheduler_thread = Thread.new do
415
-
416
- Thread.current[:name] = @thread_name
417
-
418
- if defined?(JRUBY_VERSION)
419
- require 'java'
420
- java.lang.Thread.current_thread.name = @thread_name
421
- end
422
-
423
- loop do
424
-
425
- break if @stopped
426
-
427
- t0 = Time.now.to_f
428
-
429
- step
430
-
431
- d = Time.now.to_f - t0 # + @correction
432
-
433
- next if d > @precision
434
-
435
- sleep(@precision - d)
436
- end
437
- end
438
- end
439
-
440
- #
441
- # Instantiates a new Rufus::Scheduler instance, starts it and returns it
442
- #
443
- def self.start_new (params = {})
444
-
445
- s = self.new(params)
446
- s.start
447
- s
448
- end
449
-
450
- #
451
- # The scheduler is stoppable via sstop()
452
- #
453
- def stop
454
-
455
- @stopped = true
456
- end
457
-
458
- # (for backward compatibility)
459
- #
460
- alias :sstart :start
461
-
462
- # (for backward compatibility)
463
- #
464
- alias :sstop :stop
465
-
466
- #
467
- # Joins on the scheduler thread
468
- #
469
- def join
470
-
471
- @scheduler_thread.join
472
- end
473
-
474
- #
475
- # Like join() but takes care of setting the 'exit_when_no_more_jobs'
476
- # attribute of this scheduler to true before joining.
477
- # Thus the scheduler will exit (and the join terminates) as soon as
478
- # there aren't no more 'at' (or 'every') jobs in the scheduler.
479
- #
480
- # Currently used only in unit tests.
481
- #
482
- def join_until_no_more_jobs
483
-
484
- @exit_when_no_more_jobs = true
485
- join
486
- end
487
-
488
- #
489
- # Ensures that a duration is a expressed as a Float instance.
490
- #
491
- # duration_to_f("10s")
492
- #
493
- # will yield 10.0
494
- #
495
- def duration_to_f (s)
496
-
497
- Rufus.duration_to_f(s)
498
- end
499
-
500
- #--
501
- #
502
- # The scheduling methods
503
- #
504
- #++
505
-
506
- #
507
- # Schedules a job by specifying at which time it should trigger.
508
- # Returns the a job_id that can be used to unschedule the job.
509
- #
510
- # This method returns a job identifier which can be used to unschedule()
511
- # the job.
512
- #
513
- # If the job is specified in the past, it will be triggered immediately
514
- # but not scheduled.
515
- # To avoid the triggering, the parameter :discard_past may be set to
516
- # true :
517
- #
518
- # jobid = scheduler.schedule_at(yesterday, :discard_past => true) do
519
- # puts "you'll never read this message"
520
- # end
521
- #
522
- # And 'jobid' will hold a nil (not scheduled).
523
- #
524
- #
525
- def schedule_at (at, params={}, &block)
526
-
527
- do_schedule_at(
528
- at,
529
- prepare_params(params),
530
- &block)
531
- end
532
-
533
- #
534
- # a shortcut for schedule_at
535
- #
536
- alias :at :schedule_at
537
-
538
-
539
- #
540
- # Schedules a job by stating in how much time it should trigger.
541
- # Returns the a job_id that can be used to unschedule the job.
542
- #
543
- # This method returns a job identifier which can be used to unschedule()
544
- # the job.
545
- #
546
- def schedule_in (duration, params={}, &block)
547
-
548
- do_schedule_at(
549
- Time.new.to_f + Rufus.duration_to_f(duration),
550
- prepare_params(params),
551
- &block)
552
- end
553
-
554
- #
555
- # a shortcut for schedule_in
556
- #
557
- alias :in :schedule_in
558
-
559
- #
560
- # Schedules a job in a loop. After an execution, it will not execute
561
- # before the time specified in 'freq'.
562
- #
563
- # This method returns a job identifier which can be used to unschedule()
564
- # the job.
565
- #
566
- # In case of exception in the job, it will be rescheduled. If you don't
567
- # want the job to be rescheduled, set the parameter :try_again to false.
568
- #
569
- # scheduler.schedule_every "500", :try_again => false do
570
- # do_some_prone_to_error_stuff()
571
- # # won't get rescheduled in case of exception
572
- # end
573
- #
574
- # Since rufus-scheduler 1.0.2, the params :first_at and :first_in are
575
- # accepted.
576
- #
577
- # scheduler.schedule_every "2d", :first_in => "5h" do
578
- # # schedule something every two days, start in 5 hours...
579
- # end
580
- #
581
- # (without setting a :first_in (or :first_at), our example schedule would
582
- # have had been triggered after two days).
583
- #
584
- def schedule_every (freq, params={}, &block)
585
-
586
- params = prepare_params(params)
587
- params[:every] = freq
588
-
589
- first_at = params[:first_at]
590
- first_in = params[:first_in]
591
-
592
- #params[:delayed] = true if first_at or first_in
593
-
594
- first_at = if first_at
595
- at_to_f(first_at)
596
- elsif first_in
597
- Time.now.to_f + Rufus.duration_to_f(first_in)
598
- else
599
- Time.now.to_f + Rufus.duration_to_f(freq) # not triggering immediately
600
- end
601
-
602
- do_schedule_at(first_at, params, &block)
603
- end
604
-
605
- #
606
- # a shortcut for schedule_every
607
- #
608
- alias :every :schedule_every
609
-
610
- #
611
- # Schedules a cron job, the 'cron_line' is a string
612
- # following the Unix cron standard (see "man 5 crontab" in your command
613
- # line, or http://www.google.com/search?q=man%205%20crontab).
614
- #
615
- # For example :
616
- #
617
- # scheduler.schedule("5 0 * * *", s)
618
- # # will trigger the schedulable s every day
619
- # # five minutes after midnight
620
- #
621
- # scheduler.schedule("15 14 1 * *", s)
622
- # # will trigger s at 14:15 on the first of every month
623
- #
624
- # scheduler.schedule("0 22 * * 1-5") do
625
- # puts "it's break time..."
626
- # end
627
- # # outputs a message every weekday at 10pm
628
- #
629
- # Returns the job id attributed to this 'cron job', this id can
630
- # be used to unschedule the job.
631
- #
632
- # This method returns a job identifier which can be used to unschedule()
633
- # the job.
634
- #
635
- def schedule (cron_line, params={}, &block)
636
-
637
- params = prepare_params(params)
638
-
639
- #
640
- # is a job with the same id already scheduled ?
641
-
642
- cron_id = params[:cron_id] || params[:job_id]
643
-
644
- #@unschedule_queue << cron_id
645
-
646
- #
647
- # schedule
648
-
649
- b = to_block(params, &block)
650
- job = CronJob.new(self, cron_id, cron_line, params, &b)
651
-
652
- @schedule_queue << job
653
-
654
- job.job_id
655
- end
656
-
657
- #
658
- # an alias for schedule()
659
- #
660
- alias :cron :schedule
661
-
662
- #--
663
- #
664
- # The UNscheduling methods
665
- #
666
- #++
667
-
668
- #
669
- # Unschedules an 'at' or a 'cron' job identified by the id
670
- # it was given at schedule time.
671
- #
672
- def unschedule (job_id)
673
-
674
- @unschedule_queue << job_id
675
- end
676
-
677
- #
678
- # Unschedules a cron job
679
- #
680
- # (deprecated : use unschedule(job_id) for all the jobs !)
681
- #
682
- def unschedule_cron_job (job_id)
683
-
684
- unschedule(job_id)
685
- end
686
-
687
- #--
688
- #
689
- # 'query' methods
690
- #
691
- #++
692
-
693
- #
694
- # Returns the job corresponding to job_id, an instance of AtJob
695
- # or CronJob will be returned.
696
- #
697
- def get_job (job_id)
698
-
699
- @cron_jobs[job_id] || @non_cron_jobs[job_id]
700
- end
701
-
702
- #
703
- # Finds a job (via get_job()) and then returns the wrapped
704
- # schedulable if any.
705
- #
706
- def get_schedulable (job_id)
707
-
708
- j = get_job(job_id)
709
- j.respond_to?(:schedulable) ? j.schedulable : nil
710
- end
711
-
712
- #
713
- # Returns an array of jobs that have the given tag.
714
- #
715
- def find_jobs (tag=nil)
716
-
717
- jobs = @cron_jobs.values + @non_cron_jobs.values
718
- jobs = jobs.select { |job| job.has_tag?(tag) } if tag
719
- jobs
720
- end
721
-
722
- #
723
- # Returns all the jobs in the scheduler.
724
- #
725
- def all_jobs
726
-
727
- find_jobs()
728
- end
729
-
730
- #
731
- # Finds the jobs with the given tag and then returns an array of
732
- # the wrapped Schedulable objects.
733
- # Jobs that haven't a wrapped Schedulable won't be included in the
734
- # result.
735
- #
736
- def find_schedulables (tag)
737
-
738
- find_jobs(tag).find_all { |job| job.respond_to?(:schedulable) }
739
- end
740
-
741
- #
742
- # Returns the number of currently pending jobs in this scheduler
743
- # ('at' jobs and 'every' jobs).
744
- #
745
- def pending_job_count
746
-
747
- @pending_jobs.size
748
- end
749
-
750
- #
751
- # Returns the number of cron jobs currently active in this scheduler.
752
- #
753
- def cron_job_count
754
-
755
- @cron_jobs.size
756
- end
757
-
758
- #
759
- # Returns the current count of 'every' jobs scheduled.
760
- #
761
- def every_job_count
762
-
763
- @non_cron_jobs.values.select { |j| j.class == EveryJob }.size
764
- end
765
-
766
- #
767
- # Returns the current count of 'at' jobs scheduled (not 'every').
768
- #
769
- def at_job_count
770
-
771
- @non_cron_jobs.values.select { |j| j.class == AtJob }.size
772
- end
773
-
774
- #
775
- # Returns true if the given string seems to be a cron string.
776
- #
777
- def self.is_cron_string (s)
778
-
779
- s.match ".+ .+ .+ .+ .+" # well...
780
- end
781
-
782
- private
783
-
784
- #
785
- # the unschedule work itself.
786
- #
787
- def do_unschedule (job_id)
788
-
789
- job = get_job job_id
790
-
791
- return (@cron_jobs.delete(job_id) != nil) if job.is_a?(CronJob)
792
-
793
- return false unless job # not found
794
-
795
- if job.is_a?(AtJob) # catches AtJob and EveryJob instances
796
- @non_cron_jobs.delete(job_id)
797
- job.params[:dont_reschedule] = true # for AtJob as well, no worries
798
- end
799
-
800
- for i in 0...@pending_jobs.length
801
- if @pending_jobs[i].job_id == job_id
802
- @pending_jobs.delete_at i
803
- return true # asap
804
- end
805
- end
806
-
807
- true
808
- end
809
-
810
- #
811
- # Making sure that params is a Hash.
812
- #
813
- def prepare_params (params)
814
-
815
- params.is_a?(Schedulable) ? { :schedulable => params } : params
816
- end
817
-
818
- #
819
- # The core method behind schedule_at and schedule_in (and also
820
- # schedule_every). It's protected, don't use it directly.
821
- #
822
- def do_schedule_at (at, params={}, &block)
823
-
824
- job = params.delete(:job)
825
-
826
- unless job
827
-
828
- jobClass = params[:every] ? EveryJob : AtJob
829
-
830
- b = to_block(params, &block)
831
-
832
- job = jobClass.new(self, at_to_f(at), params[:job_id], params, &b)
833
- end
834
-
835
- if jobClass == AtJob && job.at < (Time.new.to_f + @precision)
836
-
837
- job.trigger() unless params[:discard_past]
838
-
839
- @non_cron_jobs.delete job.job_id # just to be sure
840
-
841
- return nil
842
- end
843
-
844
- @non_cron_jobs[job.job_id] = job
845
-
846
- @schedule_queue << job
847
-
848
- job.job_id
849
- end
850
-
851
- #
852
- # Ensures an 'at' instance is translated to a float
853
- # (to be compared with the float coming from time.to_f)
854
- #
855
- def at_to_f (at)
856
-
857
- at = Rufus::to_ruby_time(at) if at.kind_of?(String)
858
- at = Rufus::to_gm_time(at) if at.kind_of?(DateTime)
859
- at = at.to_f if at.kind_of?(Time)
860
-
861
- raise "cannot schedule at : #{at.inspect}" unless at.is_a?(Float)
862
-
863
- at
864
- end
865
-
866
- #
867
- # Returns a block. If a block is passed, will return it, else,
868
- # if a :schedulable is set in the params, will return a block
869
- # wrapping a call to it.
870
- #
871
- def to_block (params, &block)
872
-
873
- return block if block
874
-
875
- schedulable = params.delete(:schedulable)
876
-
877
- return nil unless schedulable
878
-
879
- l = lambda do
880
- schedulable.trigger(params)
881
- end
882
- class << l
883
- attr_accessor :schedulable
884
- end
885
- l.schedulable = schedulable
886
-
887
- l
888
- end
889
-
890
- #
891
- # Pushes an 'at' job into the pending job list
892
- #
893
- def push_pending_job (job)
894
-
895
- old = @pending_jobs.find { |j| j.job_id == job.job_id }
896
- @pending_jobs.delete(old) if old
897
- #
898
- # override previous job with same id
899
-
900
- if @pending_jobs.length < 1 or job.at >= @pending_jobs.last.at
901
- @pending_jobs << job
902
- return
903
- end
904
-
905
- for i in 0...@pending_jobs.length
906
- if job.at <= @pending_jobs[i].at
907
- @pending_jobs[i, 0] = job
908
- return # right place found
909
- end
910
- end
911
- end
912
-
913
- #
914
- # This is the method called each time the scheduler wakes up
915
- # (by default 4 times per second). It's meant to quickly
916
- # determine if there are jobs to trigger else to get back to sleep.
917
- # 'cron' jobs get executed if necessary then 'at' jobs.
918
- #
919
- def step
920
-
921
- step_unschedule
922
- # unschedules any job in the unschedule queue before
923
- # they have a chance to get triggered.
924
-
925
- step_trigger
926
- # triggers eligible jobs
927
-
928
- step_schedule
929
- # schedule new jobs
930
-
931
- # done.
932
- end
933
-
934
- #
935
- # unschedules jobs in the unschedule_queue
936
- #
937
- def step_unschedule
938
-
939
- loop do
940
-
941
- break if @unschedule_queue.empty?
942
-
943
- do_unschedule(@unschedule_queue.pop)
944
- end
945
- end
946
-
947
- #
948
- # adds every job waiting in the @schedule_queue to
949
- # either @pending_jobs or @cron_jobs.
950
- #
951
- def step_schedule
952
-
953
- loop do
954
-
955
- break if @schedule_queue.empty?
956
-
957
- j = @schedule_queue.pop
958
-
959
- if j.is_a?(CronJob)
960
-
961
- @cron_jobs[j.job_id] = j
962
-
963
- else # it's an 'at' job
964
-
965
- push_pending_job j
966
- end
967
- end
968
- end
969
-
970
- #
971
- # triggers every eligible pending (at or every) jobs, then every eligible
972
- # cron jobs.
973
- #
974
- def step_trigger
975
-
976
- now = Time.now
977
-
978
- if @exit_when_no_more_jobs && @pending_jobs.size < 1
979
-
980
- @stopped = true
981
- return
982
- end
983
-
984
- # TODO : eventually consider running cron / pending
985
- # job triggering in two different threads
986
- #
987
- # but well... there's the synchronization issue...
988
-
989
- #
990
- # cron jobs
991
-
992
- if now.sec != @last_cron_second
993
-
994
- @last_cron_second = now.sec
995
-
996
- @cron_jobs.each do |cron_id, cron_job|
997
- #trigger(cron_job) if cron_job.matches?(now, @precision)
998
- cron_job.trigger if cron_job.matches?(now)
999
- end
1000
- end
1001
-
1002
- #
1003
- # pending jobs
1004
-
1005
- now = now.to_f
1006
- #
1007
- # that's what at jobs do understand
1008
-
1009
- loop do
1010
-
1011
- break if @pending_jobs.length < 1
1012
-
1013
- job = @pending_jobs[0]
1014
-
1015
- break if job.at > now
1016
-
1017
- #if job.at <= now
1018
- #
1019
- # obviously
1020
-
1021
- job.trigger
1022
-
1023
- @pending_jobs.delete_at 0
1024
- end
1025
- end
1026
-
1027
- #
1028
- # If an error occurs in the job, it well get caught and an error
1029
- # message will be displayed to STDOUT.
1030
- # If this scheduler provides a lwarn(message) method, it will
1031
- # be used insted.
1032
- #
1033
- # Of course, one can override this method.
1034
- #
1035
- def log_exception (e)
1036
-
1037
- message =
1038
- "trigger() caught exception\n" +
1039
- e.to_s + "\n" +
1040
- e.backtrace.join("\n")
1041
-
1042
- if self.respond_to?(:lwarn)
1043
- lwarn { message }
1044
- else
1045
- puts message
1046
- end
1047
- end
1048
- end
1049
-
1050
- #
1051
- # This module adds a trigger method to any class that includes it.
1052
- # The default implementation feature here triggers an exception.
1053
- #
1054
- module Schedulable
1055
-
1056
- def trigger (params)
1057
- raise "trigger() implementation is missing"
1058
- end
1059
-
1060
- def reschedule (scheduler)
1061
- raise "reschedule() implentation is missing"
1062
- end
1063
- end
1064
-
1065
- #
1066
- # This error is thrown when the :timeout attribute triggers
1067
- #
1068
- class TimeOutError < RuntimeError
1069
- end
1070
-
1071
- protected
1072
-
1073
- JOB_ID_LOCK = Mutex.new
1074
- #
1075
- # would it be better to use a Mutex instead of a full-blown
1076
- # Monitor ?
1077
-
1078
- #
1079
- # The parent class for scheduled jobs.
1080
- #
1081
- class Job
1082
-
1083
- @@last_given_id = 0
1084
- #
1085
- # as a scheduler is fully transient, no need to
1086
- # have persistent ids, a simple counter is sufficient
1087
-
1088
- #
1089
- # The identifier for the job
1090
- #
1091
- attr_accessor :job_id
1092
-
1093
- #
1094
- # An array of tags
1095
- #
1096
- attr_accessor :tags
1097
-
1098
- #
1099
- # The block to execute at trigger time
1100
- #
1101
- attr_accessor :block
1102
-
1103
- #
1104
- # A reference to the scheduler
1105
- #
1106
- attr_reader :scheduler
1107
-
1108
- #
1109
- # Keeping a copy of the initialization params of the job.
1110
- #
1111
- attr_reader :params
1112
-
1113
- #
1114
- # if the job is currently executing, this field points to
1115
- # the 'trigger thread'
1116
- #
1117
- attr_reader :trigger_thread
1118
-
1119
-
1120
- def initialize (scheduler, job_id, params, &block)
1121
-
1122
- @scheduler = scheduler
1123
- @block = block
1124
-
1125
- if job_id
1126
- @job_id = job_id
1127
- else
1128
- JOB_ID_LOCK.synchronize do
1129
- @job_id = @@last_given_id
1130
- @@last_given_id = @job_id + 1
1131
- end
1132
- end
1133
-
1134
- @params = params
1135
-
1136
- #@tags = Array(tags).collect { |tag| tag.to_s }
1137
- # making sure we have an array of String tags
1138
-
1139
- @tags = Array(params[:tags])
1140
- # any tag is OK
1141
- end
1142
-
1143
- #
1144
- # Returns true if this job sports the given tag
1145
- #
1146
- def has_tag? (tag)
1147
-
1148
- @tags.include?(tag)
1149
- end
1150
-
1151
- #
1152
- # Removes (cancels) this job from its scheduler.
1153
- #
1154
- def unschedule
1155
-
1156
- @scheduler.unschedule(@job_id)
1157
- end
1158
-
1159
- #
1160
- # Triggers the job (in a dedicated thread).
1161
- #
1162
- def trigger
1163
-
1164
- Thread.new do
1165
-
1166
- @trigger_thread = Thread.current
1167
- # keeping track of the thread
1168
-
1169
- begin
1170
-
1171
- do_trigger
1172
-
1173
- rescue Exception => e
1174
-
1175
- @scheduler.send(:log_exception, e)
1176
- end
1177
-
1178
- #@trigger_thread = nil if @trigger_thread = Thread.current
1179
- @trigger_thread = nil
1180
- # overlapping executions, what to do ?
1181
- end
1182
-
1183
- if trigger_thread_alive? and (to = @params[:timeout])
1184
- @scheduler.in(to, :tags => 'timeout') do
1185
- @trigger_thread.raise(Rufus::TimeOutError) if trigger_thread_alive?
1186
- end
1187
- end
1188
- end
1189
-
1190
- protected
1191
-
1192
- def trigger_thread_alive?
1193
- (@trigger_thread && @trigger_thread.alive?)
1194
- end
1195
- end
1196
-
1197
- #
1198
- # An 'at' job.
1199
- #
1200
- class AtJob < Job
1201
-
1202
- #
1203
- # The float representation (Time.to_f) of the time at which
1204
- # the job should be triggered.
1205
- #
1206
- attr_accessor :at
1207
-
1208
-
1209
- def initialize (scheduler, at, at_id, params, &block)
1210
-
1211
- super(scheduler, at_id, params, &block)
1212
- @at = at
1213
- end
1214
-
1215
- #
1216
- # Returns the Time instance at which this job is scheduled.
1217
- #
1218
- def schedule_info
1219
-
1220
- Time.at(@at)
1221
- end
1222
-
1223
- #
1224
- # next_time is last_time (except for EveryJob instances). Returns
1225
- # a Time instance.
1226
- #
1227
- def next_time
1228
-
1229
- schedule_info
1230
- end
1231
-
1232
- protected
1233
-
1234
- #
1235
- # Triggers the job (calls the block)
1236
- #
1237
- def do_trigger
1238
-
1239
- @block.call @job_id, @at
1240
-
1241
- @scheduler.instance_variable_get(:@non_cron_jobs).delete @job_id
1242
- end
1243
- end
1244
-
1245
- #
1246
- # An 'every' job is simply an extension of an 'at' job.
1247
- #
1248
- class EveryJob < AtJob
1249
-
1250
- #
1251
- # Returns the frequency string used to schedule this EveryJob,
1252
- # like for example "3d" or "1M10d3h".
1253
- #
1254
- def schedule_info
1255
-
1256
- @params[:every]
1257
- end
1258
-
1259
- protected
1260
-
1261
- #
1262
- # triggers the job, then reschedules it if necessary
1263
- #
1264
- def do_trigger
1265
-
1266
- hit_exception = false
1267
-
1268
- begin
1269
-
1270
- @block.call @job_id, @at, @params
1271
-
1272
- rescue Exception => e
1273
-
1274
- @scheduler.send(:log_exception, e)
1275
-
1276
- hit_exception = true
1277
- end
1278
-
1279
- if \
1280
- @scheduler.instance_variable_get(:@exit_when_no_more_jobs) or
1281
- (@params[:dont_reschedule] == true) or
1282
- (hit_exception and @params[:try_again] == false)
1283
-
1284
- @scheduler.instance_variable_get(:@non_cron_jobs).delete(job_id)
1285
- # maybe it'd be better to wipe that reference from here anyway...
1286
-
1287
- return
1288
- end
1289
-
1290
- #
1291
- # ok, reschedule ...
1292
-
1293
-
1294
- params[:job] = self
1295
-
1296
- @at = @at + Rufus.duration_to_f(params[:every])
1297
-
1298
- @scheduler.send(:do_schedule_at, @at, params)
1299
- end
1300
- end
1301
-
1302
- #
1303
- # A cron job.
1304
- #
1305
- class CronJob < Job
1306
-
1307
- #
1308
- # The CronLine instance representing the times at which
1309
- # the cron job has to be triggered.
1310
- #
1311
- attr_accessor :cron_line
1312
-
1313
- def initialize (scheduler, cron_id, line, params, &block)
1314
-
1315
- super(scheduler, cron_id, params, &block)
1316
-
1317
- if line.is_a?(String)
1318
-
1319
- @cron_line = CronLine.new(line)
1320
-
1321
- elsif line.is_a?(CronLine)
1322
-
1323
- @cron_line = line
1324
-
1325
- else
1326
-
1327
- raise \
1328
- "Cannot initialize a CronJob " +
1329
- "with a param of class #{line.class}"
1330
- end
1331
- end
1332
-
1333
- #
1334
- # This is the method called by the scheduler to determine if it
1335
- # has to fire this CronJob instance.
1336
- #
1337
- def matches? (time)
1338
- #def matches? (time, precision)
1339
-
1340
- #@cron_line.matches?(time, precision)
1341
- @cron_line.matches?(time)
1342
- end
1343
-
1344
- #
1345
- # Returns the original cron tab string used to schedule this
1346
- # Job. Like for example "60/3 * * * Sun".
1347
- #
1348
- def schedule_info
1349
-
1350
- @cron_line.original
1351
- end
1352
-
1353
- #
1354
- # Returns a Time instance : the next time this cron job is
1355
- # supposed to "fire".
1356
- #
1357
- # 'from' is used to specify the starting point for determining
1358
- # what will be the next time. Defaults to now.
1359
- #
1360
- def next_time (from=Time.now)
1361
-
1362
- @cron_line.next_time(from)
1363
- end
1364
-
1365
- protected
1366
-
1367
- #
1368
- # As the name implies.
1369
- #
1370
- def do_trigger
1371
-
1372
- @block.call @job_id, @cron_line, @params
1373
- end
1374
- end
1375
-
1376
- end
2
+ require 'rufus/scheduler/scheduler'
1377
3