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.
- data/CHANGELOG.txt +2 -82
- data/CREDITS.txt +8 -2
- data/README.rdoc +359 -0
- data/TODO.txt +51 -0
- data/lib/rufus-scheduler.rb +1 -1
- data/lib/rufus/otime.rb +1 -1
- data/lib/rufus/{scheduler → sc}/cronline.rb +44 -80
- data/lib/rufus/sc/jobqueues.rb +157 -0
- data/lib/rufus/sc/jobs.rb +339 -0
- data/lib/rufus/{scheduler/otime.rb → sc/rtime.rb} +35 -45
- data/lib/rufus/sc/scheduler.rb +454 -0
- data/lib/rufus/scheduler.rb +53 -1
- data/spec/spec.rb +14 -0
- metadata +14 -11
- data/README.txt +0 -118
- data/lib/rufus/scheduler/jobs.rb +0 -334
- data/lib/rufus/scheduler/scheduler.rb +0 -1082
- data/test/test.rb +0 -20
@@ -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
|
-
|