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