rufus-scheduler 1.0.12 → 1.0.13

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