rufus-scheduler 1.0.14 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
-