rufus-scheduler 1.0.14 → 2.0.0

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