rufus-scheduler 1.0.5 → 1.0.6
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 +8 -0
- data/CREDITS.txt +1 -0
- data/README.txt +7 -2
- data/lib/rufus/otime.rb +302 -302
- data/lib/rufus/scheduler.rb +1129 -1111
- data/test/cron_test.rb +81 -81
- data/test/cronline_test.rb +29 -23
- data/test/dev.rb +28 -0
- data/test/scheduler_0_test.rb +228 -228
- data/test/scheduler_1_test.rb +49 -54
- data/test/scheduler_2_test.rb +69 -69
- data/test/scheduler_3_test.rb +36 -36
- data/test/scheduler_4_test.rb +55 -55
- data/test/scheduler_5_test.rb +42 -42
- data/test/scheduler_6_test.rb +28 -26
- data/test/scheduler_name_test.rb +54 -0
- data/test/test.rb +2 -0
- data/test/time_0_test.rb +51 -51
- data/test/time_1_test.rb +44 -44
- metadata +5 -3
data/lib/rufus/scheduler.rb
CHANGED
@@ -8,10 +8,10 @@
|
|
8
8
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
9
|
# copies of the Software, and to permit persons to whom the Software is
|
10
10
|
# furnished to do so, subject to the following conditions:
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# The above copyright notice and this permission notice shall be included in
|
13
13
|
# all copies or substantial portions of the Software.
|
14
|
-
#
|
14
|
+
#
|
15
15
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
16
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
17
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
@@ -35,1397 +35,1415 @@ require 'rufus/otime'
|
|
35
35
|
|
36
36
|
module Rufus
|
37
37
|
|
38
|
+
#
|
39
|
+
# The Scheduler is used by OpenWFEru for registering 'at' and 'cron' jobs.
|
40
|
+
# 'at' jobs to execute once at a given point in time. 'cron' jobs
|
41
|
+
# execute a specified intervals.
|
42
|
+
# The two main methods are thus schedule_at() and schedule().
|
43
|
+
#
|
44
|
+
# schedule_at() and schedule() await either a Schedulable instance and
|
45
|
+
# params (usually an array or nil), either a block, which is more in the
|
46
|
+
# Ruby way.
|
47
|
+
#
|
48
|
+
# == The gem "openwferu-scheduler"
|
49
|
+
#
|
50
|
+
# This scheduler was previously known as the "openwferu-scheduler" gem.
|
51
|
+
#
|
52
|
+
# To ensure that code tapping the previous gem still runs fine with
|
53
|
+
# "rufus-scheduler", this new gem has 'pointers' for the old class
|
54
|
+
# names.
|
55
|
+
#
|
56
|
+
# require 'rubygems'
|
57
|
+
# require 'openwfe/util/scheduler'
|
58
|
+
# s = OpenWFE::Scheduler.new
|
59
|
+
#
|
60
|
+
# will still run OK with "rufus-scheduler".
|
61
|
+
#
|
62
|
+
# == Examples
|
63
|
+
#
|
64
|
+
# require 'rubygems'
|
65
|
+
# require 'rufus/scheduler'
|
66
|
+
#
|
67
|
+
#
|
68
|
+
# scheduler.schedule_in("3d") do
|
69
|
+
# regenerate_monthly_report()
|
70
|
+
# end
|
71
|
+
# #
|
72
|
+
# # will call the regenerate_monthly_report method
|
73
|
+
# # in 3 days from now
|
74
|
+
#
|
75
|
+
# scheduler.schedule "0 22 * * 1-5" do
|
76
|
+
# log.info "activating security system..."
|
77
|
+
# activate_security_system()
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# job_id = scheduler.schedule_at "Sun Oct 07 14:24:01 +0900 2009" do
|
81
|
+
# init_self_destruction_sequence()
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# 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
|
+
# The scheduler has a "exit_when_no_more_jobs" attribute. When set to
|
117
|
+
# 'true', the scheduler will exit as soon as there are no more jobs to
|
118
|
+
# run.
|
119
|
+
# Use with care though, if you create a scheduler, set this attribute
|
120
|
+
# to true and start the scheduler, the scheduler will immediately exit.
|
121
|
+
# This attribute is best used indirectly : the method
|
122
|
+
# join_until_no_more_jobs() wraps it.
|
123
|
+
#
|
124
|
+
# The :scheduler_precision can be set when instantiating the scheduler.
|
125
|
+
#
|
126
|
+
# scheduler = Rufus::Scheduler.new(:scheduler_precision => 0.500)
|
127
|
+
# scheduler.start
|
128
|
+
# #
|
129
|
+
# # instatiates a scheduler that checks its jobs twice per second
|
130
|
+
# # (the default is 4 times per second (0.250))
|
131
|
+
#
|
132
|
+
# Note that rufus-scheduler places a constraint on the values for the
|
133
|
+
# precision : 0.0 < p <= 1.0
|
134
|
+
# Thus
|
135
|
+
#
|
136
|
+
# scheduler.precision = 4.0
|
137
|
+
#
|
138
|
+
# or
|
139
|
+
#
|
140
|
+
# scheduler = Rufus::Scheduler.new :scheduler_precision => 5.0
|
141
|
+
#
|
142
|
+
# will raise an exception.
|
143
|
+
#
|
144
|
+
#
|
145
|
+
# == Tags
|
146
|
+
#
|
147
|
+
# Tags can be attached to jobs scheduled :
|
148
|
+
#
|
149
|
+
# scheduler.schedule_in "2h", :tags => "backup" do
|
150
|
+
# init_backup_sequence()
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# scheduler.schedule "0 24 * * *", :tags => "new_day" do
|
154
|
+
# do_this_or_that()
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# jobs = find_jobs 'backup'
|
158
|
+
# jobs.each { |job| job.unschedule }
|
159
|
+
#
|
160
|
+
# Multiple tags may be attached to a single job :
|
161
|
+
#
|
162
|
+
# scheduler.schedule_in "2h", :tags => [ "backup", "important" ] do
|
163
|
+
# init_backup_sequence()
|
164
|
+
# end
|
165
|
+
#
|
166
|
+
# The vanilla case for tags assume they are String instances, but nothing
|
167
|
+
# prevents you from using anything else. The scheduler has no persistence
|
168
|
+
# by itself, so no serialization issue.
|
169
|
+
#
|
170
|
+
#
|
171
|
+
# == Cron up to the second
|
172
|
+
#
|
173
|
+
# A cron schedule can be set at the second level :
|
174
|
+
#
|
175
|
+
# scheduler.schedule "7 * * * * *" do
|
176
|
+
# puts "it's now the seventh second of the minute"
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# The rufus scheduler recognizes an optional first column for second
|
180
|
+
# scheduling. This column can, like for the other columns, specify a
|
181
|
+
# value ("7"), a list of values ("7,8,9,27") or a range ("7-12").
|
182
|
+
#
|
183
|
+
# == Exceptions
|
184
|
+
#
|
185
|
+
# The rufus scheduler will output a stacktrace to the STDOUT in
|
186
|
+
# case of exception. There are two ways to change that behaviour.
|
187
|
+
#
|
188
|
+
# # 1 - providing a lwarn method to the scheduler instance :
|
189
|
+
#
|
190
|
+
# class << scheduler
|
191
|
+
# def lwarn (&block)
|
192
|
+
# puts "oops, something wrong happened : "
|
193
|
+
# puts block.call
|
194
|
+
# end
|
195
|
+
# end
|
196
|
+
#
|
197
|
+
# # 2 - overriding the [protected] method log_exception(e) :
|
198
|
+
#
|
199
|
+
# class << scheduler
|
200
|
+
# def log_exception (e)
|
201
|
+
# puts "something wrong happened : "+e.to_s
|
202
|
+
# end
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# == 'Every jobs' and rescheduling
|
206
|
+
#
|
207
|
+
# Every jobs can reschedule/unschedule themselves. A reschedule example :
|
208
|
+
#
|
209
|
+
# schedule.schedule_every "5h" do |job_id, at, params|
|
210
|
+
#
|
211
|
+
# mails = $inbox.fetch_mails
|
212
|
+
# mails.each { |m| $inbox.mark_as_spam(m) if is_spam(m) }
|
213
|
+
#
|
214
|
+
# params[:every] = if mails.size > 100
|
215
|
+
# "1h" # lots of spam, check every hour
|
216
|
+
# else
|
217
|
+
# "5h" # normal schedule, every 5 hours
|
218
|
+
# end
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# Unschedule example :
|
222
|
+
#
|
223
|
+
# schedule.schedule_every "10s" do |job_id, at, params|
|
224
|
+
# #
|
225
|
+
# # polls every 10 seconds until a mail arrives
|
226
|
+
#
|
227
|
+
# $mail = $inbox.fetch_last_mail
|
228
|
+
#
|
229
|
+
# params[:dont_reschedule] = true if $mail
|
230
|
+
# end
|
231
|
+
#
|
232
|
+
# == 'Every jobs', :first_at and :first_in
|
233
|
+
#
|
234
|
+
# Since rufus-scheduler 1.0.2, the schedule_every methods recognizes two
|
235
|
+
# optional parameters, :first_at and :first_in
|
236
|
+
#
|
237
|
+
# scheduler.schedule_every "2d", :first_in => "5h" do
|
238
|
+
# # schedule something every two days, start in 5 hours...
|
239
|
+
# end
|
240
|
+
#
|
241
|
+
# scheduler.schedule_every "2d", :first_at => "5h" do
|
242
|
+
# # schedule something every two days, start in 5 hours...
|
243
|
+
# end
|
244
|
+
#
|
245
|
+
# == :thread_name option
|
246
|
+
#
|
247
|
+
# You can specify the name of the scheduler's thread. Should make
|
248
|
+
# it easier in some debugging situations.
|
249
|
+
#
|
250
|
+
# scheduler.new :thread_name => "the crazy scheduler"
|
251
|
+
#
|
252
|
+
class Scheduler
|
253
|
+
|
38
254
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# execute a specified intervals.
|
42
|
-
# The two main methods are thus schedule_at() and schedule().
|
43
|
-
#
|
44
|
-
# schedule_at() and schedule() await either a Schedulable instance and
|
45
|
-
# params (usually an array or nil), either a block, which is more in the
|
46
|
-
# Ruby way.
|
47
|
-
#
|
48
|
-
# == The gem "openwferu-scheduler"
|
49
|
-
#
|
50
|
-
# This scheduler was previously known as the "openwferu-scheduler" gem.
|
51
|
-
#
|
52
|
-
# To ensure that code tapping the previous gem still runs fine with
|
53
|
-
# "rufus-scheduler", this new gem has 'pointers' for the old class
|
54
|
-
# names.
|
55
|
-
#
|
56
|
-
# require 'rubygems'
|
57
|
-
# require 'openwfe/util/scheduler'
|
58
|
-
# s = OpenWFE::Scheduler.new
|
59
|
-
#
|
60
|
-
# will still run OK with "rufus-scheduler".
|
61
|
-
#
|
62
|
-
# == Examples
|
63
|
-
#
|
64
|
-
# require 'rubygems'
|
65
|
-
# require 'rufus/scheduler'
|
66
|
-
#
|
67
|
-
#
|
68
|
-
# scheduler.schedule_in("3d") do
|
69
|
-
# regenerate_monthly_report()
|
70
|
-
# end
|
71
|
-
# #
|
72
|
-
# # will call the regenerate_monthly_report method
|
73
|
-
# # in 3 days from now
|
74
|
-
#
|
75
|
-
# scheduler.schedule "0 22 * * 1-5" do
|
76
|
-
# log.info "activating security system..."
|
77
|
-
# activate_security_system()
|
78
|
-
# end
|
79
|
-
#
|
80
|
-
# job_id = scheduler.schedule_at "Sun Oct 07 14:24:01 +0900 2009" do
|
81
|
-
# init_self_destruction_sequence()
|
82
|
-
# end
|
83
|
-
#
|
84
|
-
# 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
|
-
# The scheduler has a "exit_when_no_more_jobs" attribute. When set to
|
117
|
-
# 'true', the scheduler will exit as soon as there are no more jobs to
|
118
|
-
# run.
|
119
|
-
# Use with care though, if you create a scheduler, set this attribute
|
120
|
-
# to true and start the scheduler, the scheduler will immediately exit.
|
121
|
-
# This attribute is best used indirectly : the method
|
122
|
-
# join_until_no_more_jobs() wraps it.
|
123
|
-
#
|
124
|
-
# The :scheduler_precision can be set when instantiating the scheduler.
|
125
|
-
#
|
126
|
-
# scheduler = Rufus::Scheduler.new(:scheduler_precision => 0.500)
|
127
|
-
# scheduler.start
|
128
|
-
# #
|
129
|
-
# # instatiates a scheduler that checks its jobs twice per second
|
130
|
-
# # (the default is 4 times per second (0.250))
|
131
|
-
#
|
132
|
-
#
|
133
|
-
# == Tags
|
134
|
-
#
|
135
|
-
# Tags can be attached to jobs scheduled :
|
136
|
-
#
|
137
|
-
# scheduler.schedule_in "2h", :tags => "backup" do
|
138
|
-
# init_backup_sequence()
|
139
|
-
# end
|
140
|
-
#
|
141
|
-
# scheduler.schedule "0 24 * * *", :tags => "new_day" do
|
142
|
-
# do_this_or_that()
|
143
|
-
# end
|
144
|
-
#
|
145
|
-
# jobs = find_jobs 'backup'
|
146
|
-
# jobs.each { |job| job.unschedule }
|
147
|
-
#
|
148
|
-
# Multiple tags may be attached to a single job :
|
149
|
-
#
|
150
|
-
# scheduler.schedule_in "2h", :tags => [ "backup", "important" ] do
|
151
|
-
# init_backup_sequence()
|
152
|
-
# end
|
153
|
-
#
|
154
|
-
# The vanilla case for tags assume they are String instances, but nothing
|
155
|
-
# prevents you from using anything else. The scheduler has no persistence
|
156
|
-
# by itself, so no serialization issue.
|
157
|
-
#
|
158
|
-
#
|
159
|
-
# == Cron up to the second
|
160
|
-
#
|
161
|
-
# A cron schedule can be set at the second level :
|
162
|
-
#
|
163
|
-
# scheduler.schedule "7 * * * * *" do
|
164
|
-
# puts "it's now the seventh second of the minute"
|
165
|
-
# end
|
166
|
-
#
|
167
|
-
# The rufus scheduler recognizes an optional first column for second
|
168
|
-
# scheduling. This column can, like for the other columns, specify a
|
169
|
-
# value ("7"), a list of values ("7,8,9,27") or a range ("7-12").
|
170
|
-
#
|
171
|
-
# == Exceptions
|
172
|
-
#
|
173
|
-
# The rufus scheduler will output a stacktrace to the STDOUT in
|
174
|
-
# case of exception. There are two ways to change that behaviour.
|
175
|
-
#
|
176
|
-
# # 1 - providing a lwarn method to the scheduler instance :
|
177
|
-
#
|
178
|
-
# class << scheduler
|
179
|
-
# def lwarn (&block)
|
180
|
-
# puts "oops, something wrong happened : "
|
181
|
-
# puts block.call
|
182
|
-
# end
|
183
|
-
# end
|
184
|
-
#
|
185
|
-
# # 2 - overriding the [protected] method log_exception(e) :
|
186
|
-
#
|
187
|
-
# class << scheduler
|
188
|
-
# def log_exception (e)
|
189
|
-
# puts "something wrong happened : "+e.to_s
|
190
|
-
# end
|
191
|
-
# end
|
192
|
-
#
|
193
|
-
# == 'Every jobs' and rescheduling
|
194
|
-
#
|
195
|
-
# Every jobs can reschedule/unschedule themselves. A reschedule example :
|
196
|
-
#
|
197
|
-
# schedule.schedule_every "5h" do |job_id, at, params|
|
198
|
-
#
|
199
|
-
# mails = $inbox.fetch_mails
|
200
|
-
# mails.each { |m| $inbox.mark_as_spam(m) if is_spam(m) }
|
201
|
-
#
|
202
|
-
# params[:every] = if mails.size > 100
|
203
|
-
# "1h" # lots of spam, check every hour
|
204
|
-
# else
|
205
|
-
# "5h" # normal schedule, every 5 hours
|
206
|
-
# end
|
207
|
-
# end
|
208
|
-
#
|
209
|
-
# Unschedule example :
|
210
|
-
#
|
211
|
-
# schedule.schedule_every "10s" do |job_id, at, params|
|
212
|
-
# #
|
213
|
-
# # polls every 10 seconds until a mail arrives
|
214
|
-
#
|
215
|
-
# $mail = $inbox.fetch_last_mail
|
216
|
-
#
|
217
|
-
# params[:dont_reschedule] = true if $mail
|
218
|
-
# end
|
219
|
-
#
|
220
|
-
# == 'Every jobs', :first_at and :first_in
|
221
|
-
#
|
222
|
-
# Since rufus-scheduler 1.0.2, the schedule_every methods recognizes two
|
223
|
-
# optional parameters, :first_at and :first_in
|
255
|
+
# By default, the precision is 0.250, with means the scheduler
|
256
|
+
# will check for jobs to execute 4 times per second.
|
224
257
|
#
|
225
|
-
|
226
|
-
|
227
|
-
# end
|
258
|
+
attr_reader :precision
|
259
|
+
|
228
260
|
#
|
229
|
-
#
|
230
|
-
# # schedule something every two days, start in 5 hours...
|
231
|
-
# end
|
261
|
+
# Setting the precision ( 0.0 < p <= 1.0 )
|
232
262
|
#
|
233
|
-
|
263
|
+
def precision= (f)
|
234
264
|
|
235
|
-
|
236
|
-
|
237
|
-
# will check for jobs to execute 4 times per second.
|
238
|
-
#
|
239
|
-
attr_reader :precision
|
240
|
-
|
241
|
-
#
|
242
|
-
# Setting the precision ( 0.0 < p <= 1.0 )
|
243
|
-
#
|
244
|
-
def precision= (f)
|
245
|
-
|
246
|
-
raise "precision must be 0.0 < p <= 1.0" \
|
247
|
-
if f <= 0.0 or f > 1.0
|
248
|
-
|
249
|
-
@precision = f
|
250
|
-
end
|
251
|
-
|
252
|
-
#--
|
253
|
-
# Set by default at 0.00045, it's meant to minimize drift
|
254
|
-
#
|
255
|
-
#attr_accessor :correction
|
256
|
-
#++
|
257
|
-
|
258
|
-
#
|
259
|
-
# As its name implies.
|
260
|
-
#
|
261
|
-
attr_accessor :stopped
|
262
|
-
|
263
|
-
|
264
|
-
def initialize (params={})
|
265
|
-
|
266
|
-
super()
|
265
|
+
raise "precision must be 0.0 < p <= 1.0" \
|
266
|
+
if f <= 0.0 or f > 1.0
|
267
267
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
@schedule_queue = Queue.new
|
272
|
-
@unschedule_queue = Queue.new
|
273
|
-
#
|
274
|
-
# sync between the step() method and the [un]schedule
|
275
|
-
# methods is done via these queues, no more mutex
|
268
|
+
@precision = f
|
269
|
+
end
|
276
270
|
|
277
|
-
|
271
|
+
#--
|
272
|
+
# Set by default at 0.00045, it's meant to minimize drift
|
273
|
+
#
|
274
|
+
#attr_accessor :correction
|
275
|
+
#++
|
278
276
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
rescue Exception => e
|
284
|
-
# let precision at its default value
|
285
|
-
end
|
277
|
+
#
|
278
|
+
# As its name implies.
|
279
|
+
#
|
280
|
+
attr_accessor :stopped
|
286
281
|
|
287
|
-
#@correction = 0.00045
|
288
282
|
|
289
|
-
|
290
|
-
@dont_reschedule_every = false
|
283
|
+
def initialize (params={})
|
291
284
|
|
292
|
-
|
285
|
+
super()
|
293
286
|
|
294
|
-
|
295
|
-
|
287
|
+
@pending_jobs = []
|
288
|
+
@cron_jobs = {}
|
296
289
|
|
290
|
+
@schedule_queue = Queue.new
|
291
|
+
@unschedule_queue = Queue.new
|
297
292
|
#
|
298
|
-
#
|
299
|
-
#
|
300
|
-
def start
|
293
|
+
# sync between the step() method and the [un]schedule
|
294
|
+
# methods is done via these queues, no more mutex
|
301
295
|
|
302
|
-
|
296
|
+
@scheduler_thread = nil
|
303
297
|
|
304
|
-
|
298
|
+
@precision = 0.250
|
299
|
+
# every 250ms, the scheduler wakes up (default value)
|
300
|
+
begin
|
301
|
+
self.precision = Float(params[:scheduler_precision])
|
302
|
+
rescue Exception => e
|
303
|
+
# let precision at its default value
|
304
|
+
end
|
305
305
|
|
306
|
-
|
306
|
+
@thread_name = params[:thread_name] || "rufus scheduler"
|
307
307
|
|
308
|
-
|
308
|
+
#@correction = 0.00045
|
309
309
|
|
310
|
-
|
311
|
-
|
312
|
-
end
|
310
|
+
@exit_when_no_more_jobs = false
|
311
|
+
@dont_reschedule_every = false
|
313
312
|
|
314
|
-
|
313
|
+
@last_cron_second = -1
|
315
314
|
|
316
|
-
|
315
|
+
@stopped = true
|
316
|
+
end
|
317
317
|
|
318
|
-
|
318
|
+
#
|
319
|
+
# Starts this scheduler (or restart it if it was previously stopped)
|
320
|
+
#
|
321
|
+
def start
|
319
322
|
|
320
|
-
|
323
|
+
@stopped = false
|
321
324
|
|
322
|
-
|
325
|
+
@scheduler_thread = Thread.new do
|
323
326
|
|
324
|
-
|
327
|
+
Thread.current[:name] = @thread_name
|
325
328
|
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
+
if defined?(JRUBY_VERSION)
|
330
|
+
require 'java'
|
331
|
+
java.lang.Thread.current_thread.name = @thread_name
|
329
332
|
end
|
330
333
|
|
331
|
-
|
332
|
-
# The scheduler is stoppable via sstop()
|
333
|
-
#
|
334
|
-
def stop
|
334
|
+
loop do
|
335
335
|
|
336
|
-
|
337
|
-
end
|
336
|
+
break if @stopped
|
338
337
|
|
339
|
-
|
340
|
-
#
|
341
|
-
alias :sstart :start
|
338
|
+
t0 = Time.now.to_f
|
342
339
|
|
343
|
-
|
344
|
-
#
|
345
|
-
alias :sstop :stop
|
340
|
+
step
|
346
341
|
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
def join
|
342
|
+
d = Time.now.to_f - t0 # + @correction
|
343
|
+
|
344
|
+
next if d > @precision
|
351
345
|
|
352
|
-
|
346
|
+
sleep (@precision - d)
|
353
347
|
end
|
348
|
+
end
|
349
|
+
end
|
354
350
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
# there aren't no more 'at' (or 'every') jobs in the scheduler.
|
360
|
-
#
|
361
|
-
# Currently used only in unit tests.
|
362
|
-
#
|
363
|
-
def join_until_no_more_jobs
|
351
|
+
#
|
352
|
+
# The scheduler is stoppable via sstop()
|
353
|
+
#
|
354
|
+
def stop
|
364
355
|
|
365
|
-
|
366
|
-
|
367
|
-
end
|
356
|
+
@stopped = true
|
357
|
+
end
|
368
358
|
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
#
|
373
|
-
#++
|
359
|
+
# (for backward compatibility)
|
360
|
+
#
|
361
|
+
alias :sstart :start
|
374
362
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
#
|
379
|
-
# This method returns a job identifier which can be used to unschedule()
|
380
|
-
# the job.
|
381
|
-
#
|
382
|
-
# If the job is specified in the past, it will be triggered immediately
|
383
|
-
# but not scheduled.
|
384
|
-
# To avoid the triggering, the parameter :discard_past may be set to
|
385
|
-
# true :
|
386
|
-
#
|
387
|
-
# jobid = scheduler.schedule_at(yesterday, :discard_past => true) do
|
388
|
-
# puts "you'll never read this message"
|
389
|
-
# end
|
390
|
-
#
|
391
|
-
# And 'jobid' will hold a nil (not scheduled).
|
392
|
-
#
|
393
|
-
#
|
394
|
-
def schedule_at (at, params={}, &block)
|
363
|
+
# (for backward compatibility)
|
364
|
+
#
|
365
|
+
alias :sstop :stop
|
395
366
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
end
|
367
|
+
#
|
368
|
+
# Joins on the scheduler thread
|
369
|
+
#
|
370
|
+
def join
|
401
371
|
|
372
|
+
@scheduler_thread.join
|
373
|
+
end
|
402
374
|
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
375
|
+
#
|
376
|
+
# Like join() but takes care of setting the 'exit_when_no_more_jobs'
|
377
|
+
# attribute of this scheduler to true before joining.
|
378
|
+
# Thus the scheduler will exit (and the join terminates) as soon as
|
379
|
+
# there aren't no more 'at' (or 'every') jobs in the scheduler.
|
380
|
+
#
|
381
|
+
# Currently used only in unit tests.
|
382
|
+
#
|
383
|
+
def join_until_no_more_jobs
|
411
384
|
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
&block)
|
416
|
-
end
|
385
|
+
@exit_when_no_more_jobs = true
|
386
|
+
join
|
387
|
+
end
|
417
388
|
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
# the job.
|
424
|
-
#
|
425
|
-
# In case of exception in the job, it will be rescheduled. If you don't
|
426
|
-
# want the job to be rescheduled, set the parameter :try_again to false.
|
427
|
-
#
|
428
|
-
# scheduler.schedule_every "500", :try_again => false do
|
429
|
-
# do_some_prone_to_error_stuff()
|
430
|
-
# # won't get rescheduled in case of exception
|
431
|
-
# end
|
432
|
-
#
|
433
|
-
# Since rufus-scheduler 1.0.2, the params :first_at and :first_in are
|
434
|
-
# accepted.
|
435
|
-
#
|
436
|
-
# scheduler.schedule_every "2d", :first_in => "5h" do
|
437
|
-
# # schedule something every two days, start in 5 hours...
|
438
|
-
# end
|
439
|
-
#
|
440
|
-
def schedule_every (freq, params={}, &block)
|
389
|
+
#--
|
390
|
+
#
|
391
|
+
# The scheduling methods
|
392
|
+
#
|
393
|
+
#++
|
441
394
|
|
442
|
-
|
395
|
+
#
|
396
|
+
# Schedules a job by specifying at which time it should trigger.
|
397
|
+
# Returns the a job_id that can be used to unschedule the job.
|
398
|
+
#
|
399
|
+
# This method returns a job identifier which can be used to unschedule()
|
400
|
+
# the job.
|
401
|
+
#
|
402
|
+
# If the job is specified in the past, it will be triggered immediately
|
403
|
+
# but not scheduled.
|
404
|
+
# To avoid the triggering, the parameter :discard_past may be set to
|
405
|
+
# true :
|
406
|
+
#
|
407
|
+
# jobid = scheduler.schedule_at(yesterday, :discard_past => true) do
|
408
|
+
# puts "you'll never read this message"
|
409
|
+
# end
|
410
|
+
#
|
411
|
+
# And 'jobid' will hold a nil (not scheduled).
|
412
|
+
#
|
413
|
+
#
|
414
|
+
def schedule_at (at, params={}, &block)
|
443
415
|
|
444
|
-
|
445
|
-
|
446
|
-
|
416
|
+
do_schedule_at(
|
417
|
+
at,
|
418
|
+
prepare_params(params),
|
419
|
+
&block)
|
420
|
+
end
|
447
421
|
|
448
|
-
first_at = params.delete :first_at
|
449
|
-
first_in = params.delete :first_in
|
450
422
|
|
451
|
-
|
423
|
+
#
|
424
|
+
# Schedules a job by stating in how much time it should trigger.
|
425
|
+
# Returns the a job_id that can be used to unschedule the job.
|
426
|
+
#
|
427
|
+
# This method returns a job identifier which can be used to unschedule()
|
428
|
+
# the job.
|
429
|
+
#
|
430
|
+
def schedule_in (duration, params={}, &block)
|
452
431
|
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
previous_at + f
|
459
|
-
else
|
460
|
-
Time.now.to_f + f
|
461
|
-
end
|
432
|
+
do_schedule_at(
|
433
|
+
Time.new.to_f + duration_to_f(duration),
|
434
|
+
prepare_params(params),
|
435
|
+
&block)
|
436
|
+
end
|
462
437
|
|
463
|
-
|
438
|
+
#
|
439
|
+
# Schedules a job in a loop. After an execution, it will not execute
|
440
|
+
# before the time specified in 'freq'.
|
441
|
+
#
|
442
|
+
# This method returns a job identifier which can be used to unschedule()
|
443
|
+
# the job.
|
444
|
+
#
|
445
|
+
# In case of exception in the job, it will be rescheduled. If you don't
|
446
|
+
# want the job to be rescheduled, set the parameter :try_again to false.
|
447
|
+
#
|
448
|
+
# scheduler.schedule_every "500", :try_again => false do
|
449
|
+
# do_some_prone_to_error_stuff()
|
450
|
+
# # won't get rescheduled in case of exception
|
451
|
+
# end
|
452
|
+
#
|
453
|
+
# Since rufus-scheduler 1.0.2, the params :first_at and :first_in are
|
454
|
+
# accepted.
|
455
|
+
#
|
456
|
+
# scheduler.schedule_every "2d", :first_in => "5h" do
|
457
|
+
# # schedule something every two days, start in 5 hours...
|
458
|
+
# end
|
459
|
+
#
|
460
|
+
def schedule_every (freq, params={}, &block)
|
464
461
|
|
465
|
-
|
466
|
-
# trigger ...
|
462
|
+
f = duration_to_f freq
|
467
463
|
|
468
|
-
|
464
|
+
params = prepare_params params
|
465
|
+
schedulable = params[:schedulable]
|
466
|
+
params[:every] = freq
|
469
467
|
|
470
|
-
|
468
|
+
first_at = params.delete :first_at
|
469
|
+
first_in = params.delete :first_in
|
471
470
|
|
472
|
-
|
473
|
-
schedulable.trigger params
|
474
|
-
else
|
475
|
-
block.call job_id, at, params
|
476
|
-
end
|
471
|
+
previous_at = params[:previous_at]
|
477
472
|
|
478
|
-
|
473
|
+
next_at = if first_at
|
474
|
+
first_at
|
475
|
+
elsif first_in
|
476
|
+
Time.now.to_f + duration_to_f(first_in)
|
477
|
+
elsif previous_at
|
478
|
+
previous_at + f
|
479
|
+
else
|
480
|
+
Time.now.to_f + f
|
481
|
+
end
|
479
482
|
|
480
|
-
|
483
|
+
do_schedule_at(next_at, params) do |job_id, at|
|
481
484
|
|
482
|
-
|
483
|
-
|
485
|
+
#
|
486
|
+
# trigger ...
|
484
487
|
|
485
|
-
|
488
|
+
hit_exception = false
|
486
489
|
|
487
|
-
|
488
|
-
@dont_reschedule_every or
|
489
|
-
(params[:dont_reschedule] == true) or
|
490
|
-
(hit_exception and params[:try_again] == false)
|
490
|
+
begin
|
491
491
|
|
492
|
-
|
493
|
-
|
492
|
+
if schedulable
|
493
|
+
schedulable.trigger params
|
494
|
+
else
|
495
|
+
block.call job_id, at, params
|
496
|
+
end
|
494
497
|
|
495
|
-
|
496
|
-
params[:previous_at] = at
|
497
|
-
|
498
|
-
schedule_every params[:every], params, &block
|
499
|
-
#
|
500
|
-
# yes, this is a kind of recursion
|
498
|
+
rescue Exception => e
|
501
499
|
|
502
|
-
|
503
|
-
# by the block/schedulable code
|
504
|
-
end
|
500
|
+
log_exception e
|
505
501
|
|
506
|
-
|
507
|
-
end
|
502
|
+
hit_exception = true
|
508
503
|
end
|
509
504
|
|
510
|
-
#
|
511
|
-
# Schedules a cron job, the 'cron_line' is a string
|
512
|
-
# following the Unix cron standard (see "man 5 crontab" in your command
|
513
|
-
# line, or http://www.google.com/search?q=man%205%20crontab).
|
514
|
-
#
|
515
|
-
# For example :
|
516
|
-
#
|
517
|
-
# scheduler.schedule("5 0 * * *", s)
|
518
|
-
# # will trigger the schedulable s every day
|
519
|
-
# # five minutes after midnight
|
520
|
-
#
|
521
|
-
# scheduler.schedule("15 14 1 * *", s)
|
522
|
-
# # will trigger s at 14:15 on the first of every month
|
523
|
-
#
|
524
|
-
# scheduler.schedule("0 22 * * 1-5") do
|
525
|
-
# puts "it's break time..."
|
526
|
-
# end
|
527
|
-
# # outputs a message every weekday at 10pm
|
528
|
-
#
|
529
|
-
# Returns the job id attributed to this 'cron job', this id can
|
530
|
-
# be used to unschedule the job.
|
531
|
-
#
|
532
|
-
# This method returns a job identifier which can be used to unschedule()
|
533
|
-
# the job.
|
534
|
-
#
|
535
|
-
def schedule (cron_line, params={}, &block)
|
505
|
+
# cannot use a return here !!! (block)
|
536
506
|
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
507
|
+
unless \
|
508
|
+
@dont_reschedule_every or
|
509
|
+
(params[:dont_reschedule] == true) or
|
510
|
+
(hit_exception and params[:try_again] == false)
|
541
511
|
|
542
|
-
|
543
|
-
|
512
|
+
#
|
513
|
+
# ok, reschedule ...
|
544
514
|
|
545
|
-
|
546
|
-
|
515
|
+
params[:job_id] = job_id
|
516
|
+
params[:previous_at] = at
|
547
517
|
|
518
|
+
schedule_every params[:every], params, &block
|
548
519
|
#
|
549
|
-
#
|
550
|
-
|
551
|
-
b = to_block(params, &block)
|
552
|
-
job = CronJob.new(self, cron_id, cron_line, params, &b)
|
553
|
-
|
554
|
-
#@cron_jobs[job.job_id] = job
|
555
|
-
@schedule_queue << job
|
556
|
-
|
557
|
-
job.job_id
|
558
|
-
end
|
559
|
-
|
560
|
-
#--
|
561
|
-
#
|
562
|
-
# The UNscheduling methods
|
563
|
-
#
|
564
|
-
#++
|
520
|
+
# yes, this is a kind of recursion
|
565
521
|
|
566
|
-
|
567
|
-
|
568
|
-
# it was given at schedule time.
|
569
|
-
#
|
570
|
-
def unschedule (job_id)
|
571
|
-
|
572
|
-
@unschedule_queue << [ :at, job_id ]
|
522
|
+
# note that params[:every] might have been changed
|
523
|
+
# by the block/schedulable code
|
573
524
|
end
|
574
525
|
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
def unschedule_cron_job (job_id)
|
526
|
+
job_id
|
527
|
+
end
|
528
|
+
end
|
579
529
|
|
580
|
-
|
581
|
-
|
530
|
+
#
|
531
|
+
# Schedules a cron job, the 'cron_line' is a string
|
532
|
+
# following the Unix cron standard (see "man 5 crontab" in your command
|
533
|
+
# line, or http://www.google.com/search?q=man%205%20crontab).
|
534
|
+
#
|
535
|
+
# For example :
|
536
|
+
#
|
537
|
+
# scheduler.schedule("5 0 * * *", s)
|
538
|
+
# # will trigger the schedulable s every day
|
539
|
+
# # five minutes after midnight
|
540
|
+
#
|
541
|
+
# scheduler.schedule("15 14 1 * *", s)
|
542
|
+
# # will trigger s at 14:15 on the first of every month
|
543
|
+
#
|
544
|
+
# scheduler.schedule("0 22 * * 1-5") do
|
545
|
+
# puts "it's break time..."
|
546
|
+
# end
|
547
|
+
# # outputs a message every weekday at 10pm
|
548
|
+
#
|
549
|
+
# Returns the job id attributed to this 'cron job', this id can
|
550
|
+
# be used to unschedule the job.
|
551
|
+
#
|
552
|
+
# This method returns a job identifier which can be used to unschedule()
|
553
|
+
# the job.
|
554
|
+
#
|
555
|
+
def schedule (cron_line, params={}, &block)
|
582
556
|
|
583
|
-
|
584
|
-
#
|
585
|
-
# 'query' methods
|
586
|
-
#
|
587
|
-
#++
|
557
|
+
params = prepare_params(params)
|
588
558
|
|
589
|
-
|
590
|
-
|
591
|
-
# or CronJob will be returned.
|
592
|
-
#
|
593
|
-
def get_job (job_id)
|
559
|
+
#
|
560
|
+
# is a job with the same id already scheduled ?
|
594
561
|
|
595
|
-
|
596
|
-
|
562
|
+
cron_id = params[:cron_id]
|
563
|
+
cron_id = params[:job_id] unless cron_id
|
597
564
|
|
598
|
-
|
599
|
-
|
600
|
-
end
|
601
|
-
end
|
565
|
+
#unschedule(cron_id) if cron_id
|
566
|
+
@unschedule_queue << [ :cron, cron_id ]
|
602
567
|
|
603
|
-
|
604
|
-
|
605
|
-
# schedulable if any.
|
606
|
-
#
|
607
|
-
def get_schedulable (job_id)
|
568
|
+
#
|
569
|
+
# schedule
|
608
570
|
|
609
|
-
|
571
|
+
b = to_block(params, &block)
|
572
|
+
job = CronJob.new(self, cron_id, cron_line, params, &b)
|
610
573
|
|
611
|
-
|
574
|
+
#@cron_jobs[job.job_id] = job
|
575
|
+
@schedule_queue << job
|
612
576
|
|
613
|
-
|
577
|
+
job.job_id
|
578
|
+
end
|
614
579
|
|
615
|
-
|
616
|
-
|
580
|
+
#--
|
581
|
+
#
|
582
|
+
# The UNscheduling methods
|
583
|
+
#
|
584
|
+
#++
|
617
585
|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
586
|
+
#
|
587
|
+
# Unschedules an 'at' or a 'cron' job identified by the id
|
588
|
+
# it was given at schedule time.
|
589
|
+
#
|
590
|
+
def unschedule (job_id)
|
622
591
|
|
623
|
-
|
624
|
-
|
625
|
-
end
|
592
|
+
@unschedule_queue << [ :at, job_id ]
|
593
|
+
end
|
626
594
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
595
|
+
#
|
596
|
+
# Unschedules a cron job
|
597
|
+
#
|
598
|
+
def unschedule_cron_job (job_id)
|
631
599
|
|
632
|
-
|
633
|
-
|
634
|
-
# the wrapped Schedulable objects.
|
635
|
-
# Jobs that haven't a wrapped Schedulable won't be included in the
|
636
|
-
# result.
|
637
|
-
#
|
638
|
-
def find_schedulables (tag)
|
600
|
+
@unschedule_queue << [ :cron, job_id ]
|
601
|
+
end
|
639
602
|
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
#result
|
603
|
+
#--
|
604
|
+
#
|
605
|
+
# 'query' methods
|
606
|
+
#
|
607
|
+
#++
|
646
608
|
|
647
|
-
|
609
|
+
#
|
610
|
+
# Returns the job corresponding to job_id, an instance of AtJob
|
611
|
+
# or CronJob will be returned.
|
612
|
+
#
|
613
|
+
def get_job (job_id)
|
648
614
|
|
649
|
-
|
650
|
-
|
651
|
-
end
|
652
|
-
end
|
615
|
+
job = @cron_jobs[job_id]
|
616
|
+
return job if job
|
653
617
|
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
def pending_job_count
|
618
|
+
@pending_jobs.find do |job|
|
619
|
+
job.job_id == job_id
|
620
|
+
end
|
621
|
+
end
|
659
622
|
|
660
|
-
|
661
|
-
|
623
|
+
#
|
624
|
+
# Finds a job (via get_job()) and then returns the wrapped
|
625
|
+
# schedulable if any.
|
626
|
+
#
|
627
|
+
def get_schedulable (job_id)
|
662
628
|
|
663
|
-
|
664
|
-
# Returns the number of cron jobs currently active in this scheduler.
|
665
|
-
#
|
666
|
-
def cron_job_count
|
629
|
+
#return nil unless job_id
|
667
630
|
|
668
|
-
|
669
|
-
end
|
631
|
+
j = get_job(job_id)
|
670
632
|
|
671
|
-
|
672
|
-
# Returns the current count of 'every' jobs scheduled.
|
673
|
-
#
|
674
|
-
def every_job_count
|
633
|
+
return j.schedulable if j.respond_to?(:schedulable)
|
675
634
|
|
676
|
-
|
677
|
-
|
635
|
+
nil
|
636
|
+
end
|
678
637
|
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
638
|
+
#
|
639
|
+
# Returns an array of jobs that have the given tag.
|
640
|
+
#
|
641
|
+
def find_jobs (tag)
|
683
642
|
|
684
|
-
|
685
|
-
|
643
|
+
result = @cron_jobs.values.find_all do |job|
|
644
|
+
job.has_tag?(tag)
|
645
|
+
end
|
686
646
|
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
647
|
+
result + @pending_jobs.find_all do |job|
|
648
|
+
job.has_tag?(tag)
|
649
|
+
end
|
650
|
+
end
|
691
651
|
|
692
|
-
|
693
|
-
|
652
|
+
#
|
653
|
+
# Finds the jobs with the given tag and then returns an array of
|
654
|
+
# the wrapped Schedulable objects.
|
655
|
+
# Jobs that haven't a wrapped Schedulable won't be included in the
|
656
|
+
# result.
|
657
|
+
#
|
658
|
+
def find_schedulables (tag)
|
694
659
|
|
695
|
-
|
696
|
-
|
660
|
+
#jobs = find_jobs(tag)
|
661
|
+
#result = []
|
662
|
+
#jobs.each do |job|
|
663
|
+
# result.push(job.schedulable) if job.respond_to?(:schedulable)
|
664
|
+
#end
|
665
|
+
#result
|
697
666
|
|
698
|
-
|
667
|
+
find_jobs(tags).inject([]) do |result, job|
|
699
668
|
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
end
|
705
|
-
end
|
706
|
-
#
|
707
|
-
# not using delete_if because it scans the whole list
|
669
|
+
result.push(job.schedulable) if job.respond_to?(:schedulable)
|
670
|
+
result
|
671
|
+
end
|
672
|
+
end
|
708
673
|
|
709
|
-
|
710
|
-
|
674
|
+
#
|
675
|
+
# Returns the number of currently pending jobs in this scheduler
|
676
|
+
# ('at' jobs and 'every' jobs).
|
677
|
+
#
|
678
|
+
def pending_job_count
|
711
679
|
|
712
|
-
|
680
|
+
@pending_jobs.size
|
681
|
+
end
|
713
682
|
|
714
|
-
|
715
|
-
|
683
|
+
#
|
684
|
+
# Returns the number of cron jobs currently active in this scheduler.
|
685
|
+
#
|
686
|
+
def cron_job_count
|
716
687
|
|
717
|
-
|
718
|
-
|
719
|
-
#
|
720
|
-
def prepare_params (params)
|
688
|
+
@cron_jobs.size
|
689
|
+
end
|
721
690
|
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
691
|
+
#
|
692
|
+
# Returns the current count of 'every' jobs scheduled.
|
693
|
+
#
|
694
|
+
def every_job_count
|
726
695
|
|
727
|
-
|
728
|
-
|
729
|
-
# schedule_every). It's protected, don't use it directly.
|
730
|
-
#
|
731
|
-
def do_schedule_at (at, params={}, &block)
|
696
|
+
@pending_jobs.select { |j| j.is_a?(EveryJob) }.size
|
697
|
+
end
|
732
698
|
|
733
|
-
|
699
|
+
#
|
700
|
+
# Returns the current count of 'at' jobs scheduled (not 'every').
|
701
|
+
#
|
702
|
+
def at_job_count
|
734
703
|
|
735
|
-
|
704
|
+
@pending_jobs.select { |j| j.instance_of?(AtJob) }.size
|
705
|
+
end
|
736
706
|
|
737
|
-
|
707
|
+
#
|
708
|
+
# Returns true if the given string seems to be a cron string.
|
709
|
+
#
|
710
|
+
def Scheduler.is_cron_string (s)
|
738
711
|
|
739
|
-
|
712
|
+
s.match ".+ .+ .+ .+ .+"
|
713
|
+
end
|
740
714
|
|
741
|
-
|
715
|
+
#protected
|
716
|
+
private
|
742
717
|
|
743
|
-
|
718
|
+
def do_unschedule (job_id)
|
744
719
|
|
745
|
-
|
720
|
+
for i in 0...@pending_jobs.length
|
721
|
+
if @pending_jobs[i].job_id == job_id
|
722
|
+
@pending_jobs.delete_at i
|
723
|
+
return true
|
724
|
+
end
|
725
|
+
end
|
726
|
+
#
|
727
|
+
# not using delete_if because it scans the whole list
|
746
728
|
|
747
|
-
|
729
|
+
do_unschedule_cron_job job_id
|
730
|
+
end
|
748
731
|
|
749
|
-
|
732
|
+
def do_unschedule_cron_job (job_id)
|
750
733
|
|
751
|
-
|
752
|
-
|
753
|
-
end
|
734
|
+
(@cron_jobs.delete(job_id) != nil)
|
735
|
+
end
|
754
736
|
|
755
|
-
|
737
|
+
#
|
738
|
+
# Making sure that params is a Hash.
|
739
|
+
#
|
740
|
+
def prepare_params (params)
|
756
741
|
|
757
|
-
|
758
|
-
|
742
|
+
params = { :schedulable => params } \
|
743
|
+
if params.is_a?(Schedulable)
|
744
|
+
params
|
745
|
+
end
|
759
746
|
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
# will yields 10.0
|
766
|
-
#
|
767
|
-
def duration_to_f (s)
|
747
|
+
#
|
748
|
+
# The core method behind schedule_at and schedule_in (and also
|
749
|
+
# schedule_every). It's protected, don't use it directly.
|
750
|
+
#
|
751
|
+
def do_schedule_at (at, params={}, &block)
|
768
752
|
|
769
|
-
|
770
|
-
return Rufus::parse_time_string(s) if s.kind_of?(String)
|
771
|
-
Float(s.to_s)
|
772
|
-
end
|
753
|
+
#puts "0 at is '#{at.to_s}' (#{at.class})"
|
773
754
|
|
774
|
-
|
775
|
-
# Ensures an 'at' instance is translated to a float
|
776
|
-
# (to be compared with the float coming from time.to_f)
|
777
|
-
#
|
778
|
-
def at_to_f (at)
|
755
|
+
at = at_to_f at
|
779
756
|
|
780
|
-
|
781
|
-
at = Rufus::to_gm_time(at) if at.kind_of?(DateTime)
|
782
|
-
at = at.to_f if at.kind_of?(Time)
|
783
|
-
at
|
784
|
-
end
|
757
|
+
#puts "1 at is '#{at.to_s}' (#{at.class})"}"
|
785
758
|
|
786
|
-
|
787
|
-
# Returns a block. If a block is passed, will return it, else,
|
788
|
-
# if a :schedulable is set in the params, will return a block
|
789
|
-
# wrapping a call to it.
|
790
|
-
#
|
791
|
-
def to_block (params, &block)
|
759
|
+
jobClass = params[:every] ? EveryJob : AtJob
|
792
760
|
|
793
|
-
|
761
|
+
job_id = params[:job_id]
|
794
762
|
|
795
|
-
|
763
|
+
b = to_block params, &block
|
796
764
|
|
797
|
-
|
765
|
+
job = jobClass.new self, at, job_id, params, &b
|
798
766
|
|
799
|
-
|
767
|
+
#do_unschedule(job_id) if job_id
|
800
768
|
|
801
|
-
|
802
|
-
schedulable.trigger(params)
|
803
|
-
end
|
804
|
-
class << l
|
805
|
-
attr_accessor :schedulable
|
806
|
-
end
|
807
|
-
l.schedulable = schedulable
|
769
|
+
if at < (Time.new.to_f + @precision)
|
808
770
|
|
809
|
-
|
810
|
-
|
771
|
+
job.trigger() unless params[:discard_past]
|
772
|
+
return nil
|
773
|
+
end
|
811
774
|
|
812
|
-
|
813
|
-
# Pushes an 'at' job into the pending job list
|
814
|
-
#
|
815
|
-
def push_pending_job (job)
|
816
|
-
|
817
|
-
old = @pending_jobs.find { |j| j.job_id == job.job_id }
|
818
|
-
@pending_jobs.delete(old) if old
|
819
|
-
#
|
820
|
-
# override previous job with same id
|
821
|
-
|
822
|
-
if @pending_jobs.length < 1 or job.at >= @pending_jobs.last.at
|
823
|
-
@pending_jobs << job
|
824
|
-
return
|
825
|
-
end
|
826
|
-
|
827
|
-
for i in 0...@pending_jobs.length
|
828
|
-
if job.at <= @pending_jobs[i].at
|
829
|
-
@pending_jobs[i, 0] = job
|
830
|
-
return # right place found
|
831
|
-
end
|
832
|
-
end
|
833
|
-
end
|
775
|
+
@schedule_queue << job
|
834
776
|
|
835
|
-
|
836
|
-
|
837
|
-
# (by default 4 times per second). It's meant to quickly
|
838
|
-
# determine if there are jobs to trigger else to get back to sleep.
|
839
|
-
# 'cron' jobs get executed if necessary then 'at' jobs.
|
840
|
-
#
|
841
|
-
def step
|
777
|
+
job.job_id
|
778
|
+
end
|
842
779
|
|
843
|
-
|
844
|
-
|
780
|
+
#
|
781
|
+
# Ensures that a duration is a expressed as a Float instance.
|
782
|
+
#
|
783
|
+
# duration_to_f("10s")
|
784
|
+
#
|
785
|
+
# will yields 10.0
|
786
|
+
#
|
787
|
+
def duration_to_f (s)
|
845
788
|
|
846
|
-
|
847
|
-
|
848
|
-
|
789
|
+
return s if s.kind_of?(Float)
|
790
|
+
return Rufus::parse_time_string(s) if s.kind_of?(String)
|
791
|
+
Float(s.to_s)
|
792
|
+
end
|
849
793
|
|
850
|
-
|
851
|
-
|
794
|
+
#
|
795
|
+
# Ensures an 'at' instance is translated to a float
|
796
|
+
# (to be compared with the float coming from time.to_f)
|
797
|
+
#
|
798
|
+
def at_to_f (at)
|
852
799
|
|
853
|
-
|
854
|
-
|
800
|
+
at = Rufus::to_ruby_time(at) if at.kind_of?(String)
|
801
|
+
at = Rufus::to_gm_time(at) if at.kind_of?(DateTime)
|
802
|
+
at = at.to_f if at.kind_of?(Time)
|
803
|
+
at
|
804
|
+
end
|
855
805
|
|
856
|
-
|
857
|
-
|
806
|
+
#
|
807
|
+
# Returns a block. If a block is passed, will return it, else,
|
808
|
+
# if a :schedulable is set in the params, will return a block
|
809
|
+
# wrapping a call to it.
|
810
|
+
#
|
811
|
+
def to_block (params, &block)
|
858
812
|
|
859
|
-
|
860
|
-
# unschedules jobs in the unschedule_queue
|
861
|
-
#
|
862
|
-
def step_unschedule
|
813
|
+
return block if block
|
863
814
|
|
864
|
-
|
815
|
+
schedulable = params[:schedulable]
|
865
816
|
|
866
|
-
|
817
|
+
return nil unless schedulable
|
867
818
|
|
868
|
-
|
819
|
+
params.delete :schedulable
|
869
820
|
|
870
|
-
|
821
|
+
l = lambda do
|
822
|
+
schedulable.trigger(params)
|
823
|
+
end
|
824
|
+
class << l
|
825
|
+
attr_accessor :schedulable
|
826
|
+
end
|
827
|
+
l.schedulable = schedulable
|
871
828
|
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
do_unschedule job_id
|
876
|
-
end
|
877
|
-
end
|
878
|
-
end
|
829
|
+
l
|
830
|
+
end
|
879
831
|
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
def step_schedule
|
832
|
+
#
|
833
|
+
# Pushes an 'at' job into the pending job list
|
834
|
+
#
|
835
|
+
def push_pending_job (job)
|
885
836
|
|
886
|
-
|
837
|
+
old = @pending_jobs.find { |j| j.job_id == job.job_id }
|
838
|
+
@pending_jobs.delete(old) if old
|
839
|
+
#
|
840
|
+
# override previous job with same id
|
887
841
|
|
888
|
-
|
842
|
+
if @pending_jobs.length < 1 or job.at >= @pending_jobs.last.at
|
843
|
+
@pending_jobs << job
|
844
|
+
return
|
845
|
+
end
|
889
846
|
|
890
|
-
|
847
|
+
for i in 0...@pending_jobs.length
|
848
|
+
if job.at <= @pending_jobs[i].at
|
849
|
+
@pending_jobs[i, 0] = job
|
850
|
+
return # right place found
|
851
|
+
end
|
852
|
+
end
|
853
|
+
end
|
891
854
|
|
892
|
-
|
855
|
+
#
|
856
|
+
# This is the method called each time the scheduler wakes up
|
857
|
+
# (by default 4 times per second). It's meant to quickly
|
858
|
+
# determine if there are jobs to trigger else to get back to sleep.
|
859
|
+
# 'cron' jobs get executed if necessary then 'at' jobs.
|
860
|
+
#
|
861
|
+
def step
|
893
862
|
|
894
|
-
|
863
|
+
#puts Time.now.to_f
|
864
|
+
#puts @pending_jobs.collect { |j| [ j.job_id, j.at ] }.inspect
|
895
865
|
|
896
|
-
|
866
|
+
step_unschedule
|
867
|
+
# unschedules any job in the unschedule queue before
|
868
|
+
# they have a chance to get triggered.
|
897
869
|
|
898
|
-
|
899
|
-
|
900
|
-
end
|
901
|
-
end
|
870
|
+
step_trigger
|
871
|
+
# triggers eligible jobs
|
902
872
|
|
903
|
-
|
904
|
-
|
905
|
-
# cron jobs.
|
906
|
-
#
|
907
|
-
def step_trigger
|
873
|
+
step_schedule
|
874
|
+
# schedule new jobs
|
908
875
|
|
909
|
-
|
876
|
+
# done.
|
877
|
+
end
|
910
878
|
|
911
|
-
|
879
|
+
#
|
880
|
+
# unschedules jobs in the unschedule_queue
|
881
|
+
#
|
882
|
+
def step_unschedule
|
912
883
|
|
913
|
-
|
884
|
+
loop do
|
914
885
|
|
915
|
-
|
916
|
-
return
|
917
|
-
end
|
886
|
+
break if @unschedule_queue.empty?
|
918
887
|
|
919
|
-
|
920
|
-
end
|
888
|
+
type, job_id = @unschedule_queue.pop
|
921
889
|
|
922
|
-
|
923
|
-
# job triggering in two different threads
|
924
|
-
#
|
925
|
-
# but well... there's the synchronization issue...
|
890
|
+
if type == :cron
|
926
891
|
|
927
|
-
|
928
|
-
|
892
|
+
do_unschedule_cron_job job_id
|
893
|
+
else
|
929
894
|
|
930
|
-
|
895
|
+
do_unschedule job_id
|
896
|
+
end
|
897
|
+
end
|
898
|
+
end
|
931
899
|
|
932
|
-
|
900
|
+
#
|
901
|
+
# adds every job waiting in the @schedule_queue to
|
902
|
+
# either @pending_jobs or @cron_jobs.
|
903
|
+
#
|
904
|
+
def step_schedule
|
933
905
|
|
934
|
-
|
906
|
+
loop do
|
935
907
|
|
936
|
-
|
937
|
-
#puts "step() cron_id : #{cron_id}"
|
938
|
-
#trigger(cron_job) if cron_job.matches?(now, @precision)
|
939
|
-
trigger(cron_job) if cron_job.matches?(now)
|
940
|
-
end
|
941
|
-
end
|
908
|
+
break if @schedule_queue.empty?
|
942
909
|
|
943
|
-
|
944
|
-
# pending jobs
|
910
|
+
j = @schedule_queue.pop
|
945
911
|
|
946
|
-
|
947
|
-
#
|
948
|
-
# that's what at jobs do understand
|
912
|
+
if j.is_a?(CronJob)
|
949
913
|
|
950
|
-
|
914
|
+
@cron_jobs[j.job_id] = j
|
951
915
|
|
952
|
-
|
916
|
+
else # it's an 'at' job
|
953
917
|
|
954
|
-
|
918
|
+
push_pending_job j
|
919
|
+
end
|
920
|
+
end
|
921
|
+
end
|
955
922
|
|
956
|
-
|
923
|
+
#
|
924
|
+
# triggers every eligible pending jobs, then every eligible
|
925
|
+
# cron jobs.
|
926
|
+
#
|
927
|
+
def step_trigger
|
957
928
|
|
958
|
-
|
959
|
-
#
|
960
|
-
# obviously
|
929
|
+
now = Time.new
|
961
930
|
|
962
|
-
|
931
|
+
if @exit_when_no_more_jobs
|
963
932
|
|
964
|
-
|
965
|
-
end
|
966
|
-
end
|
933
|
+
if @pending_jobs.size < 1
|
967
934
|
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
def trigger (job)
|
935
|
+
@stopped = true
|
936
|
+
return
|
937
|
+
end
|
972
938
|
|
973
|
-
|
974
|
-
|
939
|
+
@dont_reschedule_every = true if at_job_count < 1
|
940
|
+
end
|
975
941
|
|
976
|
-
|
942
|
+
# TODO : eventually consider running cron / pending
|
943
|
+
# job triggering in two different threads
|
944
|
+
#
|
945
|
+
# but well... there's the synchronization issue...
|
977
946
|
|
978
|
-
|
947
|
+
#
|
948
|
+
# cron jobs
|
979
949
|
|
980
|
-
|
981
|
-
end
|
982
|
-
end
|
983
|
-
end
|
950
|
+
if now.sec != @last_cron_second
|
984
951
|
|
985
|
-
|
986
|
-
# If an error occurs in the job, it well get caught and an error
|
987
|
-
# message will be displayed to STDOUT.
|
988
|
-
# If this scheduler provides a lwarn(message) method, it will
|
989
|
-
# be used insted.
|
990
|
-
#
|
991
|
-
# Of course, one can override this method.
|
992
|
-
#
|
993
|
-
def log_exception (e)
|
994
|
-
|
995
|
-
message =
|
996
|
-
"trigger() caught exception\n" +
|
997
|
-
e.to_s + "\n" +
|
998
|
-
e.backtrace.join("\n")
|
999
|
-
|
1000
|
-
if self.respond_to?(:lwarn)
|
1001
|
-
lwarn { message }
|
1002
|
-
else
|
1003
|
-
puts message
|
1004
|
-
end
|
1005
|
-
end
|
1006
|
-
end
|
952
|
+
@last_cron_second = now.sec
|
1007
953
|
|
1008
|
-
|
1009
|
-
# This module adds a trigger method to any class that includes it.
|
1010
|
-
# The default implementation feature here triggers an exception.
|
1011
|
-
#
|
1012
|
-
module Schedulable
|
954
|
+
#puts "step() @cron_jobs.size #{@cron_jobs.size}"
|
1013
955
|
|
1014
|
-
|
1015
|
-
|
956
|
+
@cron_jobs.each do |cron_id, cron_job|
|
957
|
+
#puts "step() cron_id : #{cron_id}"
|
958
|
+
#trigger(cron_job) if cron_job.matches?(now, @precision)
|
959
|
+
trigger(cron_job) if cron_job.matches?(now)
|
960
|
+
end
|
1016
961
|
end
|
1017
962
|
|
1018
|
-
|
1019
|
-
|
1020
|
-
end
|
1021
|
-
end
|
963
|
+
#
|
964
|
+
# pending jobs
|
1022
965
|
|
1023
|
-
|
966
|
+
now = now.to_f
|
967
|
+
#
|
968
|
+
# that's what at jobs do understand
|
1024
969
|
|
1025
|
-
|
1026
|
-
#
|
1027
|
-
# would it be better to use a Mutex instead of a full-blown
|
1028
|
-
# Monitor ?
|
970
|
+
loop do
|
1029
971
|
|
1030
|
-
|
1031
|
-
# The parent class for scheduled jobs.
|
1032
|
-
#
|
1033
|
-
class Job
|
972
|
+
break if @pending_jobs.length < 1
|
1034
973
|
|
1035
|
-
|
1036
|
-
#
|
1037
|
-
# as a scheduler is fully transient, no need to
|
1038
|
-
# have persistent ids, a simple counter is sufficient
|
974
|
+
job = @pending_jobs[0]
|
1039
975
|
|
1040
|
-
|
1041
|
-
# The identifier for the job
|
1042
|
-
#
|
1043
|
-
attr_accessor :job_id
|
976
|
+
break if job.at > now
|
1044
977
|
|
978
|
+
#if job.at <= now
|
1045
979
|
#
|
1046
|
-
#
|
1047
|
-
#
|
1048
|
-
attr_accessor :tags
|
980
|
+
# obviously
|
1049
981
|
|
1050
|
-
|
1051
|
-
# The block to execute at trigger time
|
1052
|
-
#
|
1053
|
-
attr_accessor :block
|
982
|
+
trigger job
|
1054
983
|
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
attr_reader :scheduler
|
984
|
+
@pending_jobs.delete_at 0
|
985
|
+
end
|
986
|
+
end
|
1059
987
|
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
988
|
+
#
|
989
|
+
# Triggers the job (in a dedicated thread).
|
990
|
+
#
|
991
|
+
def trigger (job)
|
1065
992
|
|
1066
|
-
|
993
|
+
Thread.new do
|
994
|
+
begin
|
1067
995
|
|
1068
|
-
|
1069
|
-
@block = block
|
996
|
+
job.trigger
|
1070
997
|
|
1071
|
-
|
1072
|
-
@job_id = job_id
|
1073
|
-
else
|
1074
|
-
JOB_ID_LOCK.synchronize do
|
1075
|
-
@job_id = @@last_given_id
|
1076
|
-
@@last_given_id = @job_id + 1
|
1077
|
-
end
|
1078
|
-
end
|
998
|
+
rescue Exception => e
|
1079
999
|
|
1080
|
-
|
1000
|
+
log_exception e
|
1001
|
+
end
|
1002
|
+
end
|
1003
|
+
end
|
1004
|
+
|
1005
|
+
#
|
1006
|
+
# If an error occurs in the job, it well get caught and an error
|
1007
|
+
# message will be displayed to STDOUT.
|
1008
|
+
# If this scheduler provides a lwarn(message) method, it will
|
1009
|
+
# be used insted.
|
1010
|
+
#
|
1011
|
+
# Of course, one can override this method.
|
1012
|
+
#
|
1013
|
+
def log_exception (e)
|
1014
|
+
|
1015
|
+
message =
|
1016
|
+
"trigger() caught exception\n" +
|
1017
|
+
e.to_s + "\n" +
|
1018
|
+
e.backtrace.join("\n")
|
1019
|
+
|
1020
|
+
if self.respond_to?(:lwarn)
|
1021
|
+
lwarn { message }
|
1022
|
+
else
|
1023
|
+
puts message
|
1024
|
+
end
|
1025
|
+
end
|
1026
|
+
end
|
1081
1027
|
|
1082
|
-
|
1083
|
-
|
1028
|
+
#
|
1029
|
+
# This module adds a trigger method to any class that includes it.
|
1030
|
+
# The default implementation feature here triggers an exception.
|
1031
|
+
#
|
1032
|
+
module Schedulable
|
1084
1033
|
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1034
|
+
def trigger (params)
|
1035
|
+
raise "trigger() implementation is missing"
|
1036
|
+
end
|
1088
1037
|
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1038
|
+
def reschedule (scheduler)
|
1039
|
+
raise "reschedule() implentation is missing"
|
1040
|
+
end
|
1041
|
+
end
|
1093
1042
|
|
1094
|
-
|
1095
|
-
end
|
1043
|
+
protected
|
1096
1044
|
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1045
|
+
JOB_ID_LOCK = Monitor.new
|
1046
|
+
#
|
1047
|
+
# would it be better to use a Mutex instead of a full-blown
|
1048
|
+
# Monitor ?
|
1101
1049
|
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1050
|
+
#
|
1051
|
+
# The parent class for scheduled jobs.
|
1052
|
+
#
|
1053
|
+
class Job
|
1105
1054
|
|
1055
|
+
@@last_given_id = 0
|
1106
1056
|
#
|
1107
|
-
#
|
1108
|
-
#
|
1109
|
-
class AtJob < Job
|
1057
|
+
# as a scheduler is fully transient, no need to
|
1058
|
+
# have persistent ids, a simple counter is sufficient
|
1110
1059
|
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
attr_accessor :at
|
1116
|
-
|
1117
|
-
#
|
1118
|
-
# The constructor.
|
1119
|
-
#
|
1120
|
-
def initialize (scheduler, at, at_id, params, &block)
|
1060
|
+
#
|
1061
|
+
# The identifier for the job
|
1062
|
+
#
|
1063
|
+
attr_accessor :job_id
|
1121
1064
|
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1065
|
+
#
|
1066
|
+
# An array of tags
|
1067
|
+
#
|
1068
|
+
attr_accessor :tags
|
1125
1069
|
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1070
|
+
#
|
1071
|
+
# The block to execute at trigger time
|
1072
|
+
#
|
1073
|
+
attr_accessor :block
|
1130
1074
|
|
1131
|
-
|
1132
|
-
|
1075
|
+
#
|
1076
|
+
# A reference to the scheduler
|
1077
|
+
#
|
1078
|
+
attr_reader :scheduler
|
1133
1079
|
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1080
|
+
#
|
1081
|
+
# Keeping a copy of the initialization params of the job.
|
1082
|
+
#
|
1083
|
+
attr_reader :params
|
1138
1084
|
|
1139
|
-
Time.at(@at)
|
1140
|
-
end
|
1141
|
-
end
|
1142
1085
|
|
1143
|
-
|
1144
|
-
# An 'every' job is simply an extension of an 'at' job.
|
1145
|
-
#
|
1146
|
-
class EveryJob < AtJob
|
1086
|
+
def initialize (scheduler, job_id, params, &block)
|
1147
1087
|
|
1148
|
-
|
1149
|
-
|
1150
|
-
# like for example "3d" or "1M10d3h".
|
1151
|
-
#
|
1152
|
-
def schedule_info
|
1088
|
+
@scheduler = scheduler
|
1089
|
+
@block = block
|
1153
1090
|
|
1154
|
-
|
1155
|
-
|
1091
|
+
if job_id
|
1092
|
+
@job_id = job_id
|
1093
|
+
else
|
1094
|
+
JOB_ID_LOCK.synchronize do
|
1095
|
+
@job_id = @@last_given_id
|
1096
|
+
@@last_given_id = @job_id + 1
|
1097
|
+
end
|
1156
1098
|
end
|
1157
1099
|
|
1158
|
-
|
1159
|
-
# A cron job.
|
1160
|
-
#
|
1161
|
-
class CronJob < Job
|
1100
|
+
@params = params
|
1162
1101
|
|
1163
|
-
|
1164
|
-
|
1165
|
-
# the cron job has to be triggered.
|
1166
|
-
#
|
1167
|
-
attr_accessor :cron_line
|
1102
|
+
#@tags = Array(tags).collect { |tag| tag.to_s }
|
1103
|
+
# making sure we have an array of String tags
|
1168
1104
|
|
1169
|
-
|
1105
|
+
@tags = Array(params[:tags])
|
1106
|
+
# any tag is OK
|
1107
|
+
end
|
1170
1108
|
|
1171
|
-
|
1109
|
+
#
|
1110
|
+
# Returns true if this job sports the given tag
|
1111
|
+
#
|
1112
|
+
def has_tag? (tag)
|
1172
1113
|
|
1173
|
-
|
1114
|
+
@tags.include?(tag)
|
1115
|
+
end
|
1174
1116
|
|
1175
|
-
|
1117
|
+
#
|
1118
|
+
# Removes (cancels) this job from its scheduler.
|
1119
|
+
#
|
1120
|
+
def unschedule
|
1176
1121
|
|
1177
|
-
|
1122
|
+
@scheduler.unschedule(@job_id)
|
1123
|
+
end
|
1124
|
+
end
|
1178
1125
|
|
1179
|
-
|
1126
|
+
#
|
1127
|
+
# An 'at' job.
|
1128
|
+
#
|
1129
|
+
class AtJob < Job
|
1130
|
+
|
1131
|
+
#
|
1132
|
+
# The float representation (Time.to_f) of the time at which
|
1133
|
+
# the job should be triggered.
|
1134
|
+
#
|
1135
|
+
attr_accessor :at
|
1136
|
+
|
1137
|
+
#
|
1138
|
+
# The constructor.
|
1139
|
+
#
|
1140
|
+
def initialize (scheduler, at, at_id, params, &block)
|
1141
|
+
|
1142
|
+
super(scheduler, at_id, params, &block)
|
1143
|
+
@at = at
|
1144
|
+
end
|
1145
|
+
|
1146
|
+
#
|
1147
|
+
# Triggers the job (calls the block)
|
1148
|
+
#
|
1149
|
+
def trigger
|
1150
|
+
|
1151
|
+
@block.call @job_id, @at
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
#
|
1155
|
+
# Returns the Time instance at which this job is scheduled.
|
1156
|
+
#
|
1157
|
+
def schedule_info
|
1158
|
+
|
1159
|
+
Time.at(@at)
|
1160
|
+
end
|
1161
|
+
end
|
1180
1162
|
|
1181
|
-
|
1163
|
+
#
|
1164
|
+
# An 'every' job is simply an extension of an 'at' job.
|
1165
|
+
#
|
1166
|
+
class EveryJob < AtJob
|
1182
1167
|
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1168
|
+
#
|
1169
|
+
# Returns the frequency string used to schedule this EveryJob,
|
1170
|
+
# like for example "3d" or "1M10d3h".
|
1171
|
+
#
|
1172
|
+
def schedule_info
|
1188
1173
|
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
#
|
1193
|
-
def matches? (time)
|
1194
|
-
#def matches? (time, precision)
|
1174
|
+
@params[:every]
|
1175
|
+
end
|
1176
|
+
end
|
1195
1177
|
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1178
|
+
#
|
1179
|
+
# A cron job.
|
1180
|
+
#
|
1181
|
+
class CronJob < Job
|
1199
1182
|
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1183
|
+
#
|
1184
|
+
# The CronLine instance representing the times at which
|
1185
|
+
# the cron job has to be triggered.
|
1186
|
+
#
|
1187
|
+
attr_accessor :cron_line
|
1204
1188
|
|
1205
|
-
|
1206
|
-
end
|
1189
|
+
def initialize (scheduler, cron_id, line, params, &block)
|
1207
1190
|
|
1208
|
-
|
1209
|
-
# Returns the original cron tab string used to schedule this
|
1210
|
-
# Job. Like for example "60/3 * * * Sun".
|
1211
|
-
#
|
1212
|
-
def schedule_info
|
1191
|
+
super(scheduler, cron_id, params, &block)
|
1213
1192
|
|
1214
|
-
|
1215
|
-
end
|
1216
|
-
end
|
1193
|
+
if line.is_a?(String)
|
1217
1194
|
|
1218
|
-
|
1219
|
-
# A 'cron line' is a line in the sense of a crontab
|
1220
|
-
# (man 5 crontab) file line.
|
1221
|
-
#
|
1222
|
-
class CronLine
|
1195
|
+
@cron_line = CronLine.new(line)
|
1223
1196
|
|
1224
|
-
|
1225
|
-
# The string used for creating this cronline instance.
|
1226
|
-
#
|
1227
|
-
attr_reader :original
|
1197
|
+
elsif line.is_a?(CronLine)
|
1228
1198
|
|
1229
|
-
|
1230
|
-
:seconds,
|
1231
|
-
:minutes,
|
1232
|
-
:hours,
|
1233
|
-
:days,
|
1234
|
-
:months,
|
1235
|
-
:weekdays
|
1199
|
+
@cron_line = line
|
1236
1200
|
|
1237
|
-
|
1201
|
+
else
|
1238
1202
|
|
1239
|
-
|
1203
|
+
raise \
|
1204
|
+
"Cannot initialize a CronJob " +
|
1205
|
+
"with a param of class #{line.class}"
|
1206
|
+
end
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
#
|
1210
|
+
# This is the method called by the scheduler to determine if it
|
1211
|
+
# has to fire this CronJob instance.
|
1212
|
+
#
|
1213
|
+
def matches? (time)
|
1214
|
+
#def matches? (time, precision)
|
1215
|
+
|
1216
|
+
#@cron_line.matches?(time, precision)
|
1217
|
+
@cron_line.matches?(time)
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
#
|
1221
|
+
# As the name implies.
|
1222
|
+
#
|
1223
|
+
def trigger
|
1224
|
+
|
1225
|
+
@block.call @job_id, @cron_line
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
#
|
1229
|
+
# Returns the original cron tab string used to schedule this
|
1230
|
+
# Job. Like for example "60/3 * * * Sun".
|
1231
|
+
#
|
1232
|
+
def schedule_info
|
1233
|
+
|
1234
|
+
@cron_line.original
|
1235
|
+
end
|
1236
|
+
end
|
1240
1237
|
|
1241
|
-
|
1238
|
+
#
|
1239
|
+
# A 'cron line' is a line in the sense of a crontab
|
1240
|
+
# (man 5 crontab) file line.
|
1241
|
+
#
|
1242
|
+
class CronLine
|
1242
1243
|
|
1243
|
-
|
1244
|
+
#
|
1245
|
+
# The string used for creating this cronline instance.
|
1246
|
+
#
|
1247
|
+
attr_reader :original
|
1244
1248
|
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1249
|
+
attr_reader \
|
1250
|
+
:seconds,
|
1251
|
+
:minutes,
|
1252
|
+
:hours,
|
1253
|
+
:days,
|
1254
|
+
:months,
|
1255
|
+
:weekdays
|
1250
1256
|
|
1251
|
-
|
1257
|
+
def initialize (line)
|
1252
1258
|
|
1253
|
-
|
1254
|
-
parse_item(items[0], 0, 59)
|
1255
|
-
else
|
1256
|
-
[ 0 ]
|
1257
|
-
end
|
1258
|
-
@minutes = parse_item(items[0+offset], 0, 59)
|
1259
|
-
@hours = parse_item(items[1+offset], 0, 24)
|
1260
|
-
@days = parse_item(items[2+offset], 1, 31)
|
1261
|
-
@months = parse_item(items[3+offset], 1, 12)
|
1262
|
-
@weekdays = parse_weekdays(items[4+offset])
|
1259
|
+
super()
|
1263
1260
|
|
1264
|
-
|
1265
|
-
end
|
1261
|
+
@original = line
|
1266
1262
|
|
1267
|
-
|
1268
|
-
# Returns true if the given time matches this cron line.
|
1269
|
-
#
|
1270
|
-
# (the precision is passed as well to determine if it's
|
1271
|
-
# worth checking seconds and minutes)
|
1272
|
-
#
|
1273
|
-
def matches? (time)
|
1274
|
-
#def matches? (time, precision)
|
1275
|
-
|
1276
|
-
time = Time.at(time) \
|
1277
|
-
if time.kind_of?(Float) or time.kind_of?(Integer)
|
1278
|
-
|
1279
|
-
return false \
|
1280
|
-
if no_match?(time.sec, @seconds)
|
1281
|
-
#if precision <= 1 and no_match?(time.sec, @seconds)
|
1282
|
-
return false \
|
1283
|
-
if no_match?(time.min, @minutes)
|
1284
|
-
#if precision <= 60 and no_match?(time.min, @minutes)
|
1285
|
-
return false \
|
1286
|
-
if no_match?(time.hour, @hours)
|
1287
|
-
return false \
|
1288
|
-
if no_match?(time.day, @days)
|
1289
|
-
return false \
|
1290
|
-
if no_match?(time.month, @months)
|
1291
|
-
return false \
|
1292
|
-
if no_match?(time.wday, @weekdays)
|
1293
|
-
|
1294
|
-
true
|
1295
|
-
end
|
1263
|
+
items = line.split
|
1296
1264
|
|
1297
|
-
|
1298
|
-
|
1299
|
-
#
|
1300
|
-
#
|
1301
|
-
|
1302
|
-
def to_array
|
1303
|
-
[ @seconds, @minutes, @hours, @days, @months, @weekdays ]
|
1304
|
-
end
|
1265
|
+
unless [ 5, 6 ].include?(items.length)
|
1266
|
+
raise \
|
1267
|
+
"cron '#{line}' string should hold 5 or 6 items, " +
|
1268
|
+
"not #{items.length}" \
|
1269
|
+
end
|
1305
1270
|
|
1306
|
-
|
1271
|
+
offset = items.length - 5
|
1307
1272
|
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1273
|
+
@seconds = if offset == 1
|
1274
|
+
parse_item(items[0], 0, 59)
|
1275
|
+
else
|
1276
|
+
[ 0 ]
|
1277
|
+
end
|
1278
|
+
@minutes = parse_item(items[0+offset], 0, 59)
|
1279
|
+
@hours = parse_item(items[1+offset], 0, 24)
|
1280
|
+
@days = parse_item(items[2+offset], 1, 31)
|
1281
|
+
@months = parse_item(items[3+offset], 1, 12)
|
1282
|
+
@weekdays = parse_weekdays(items[4+offset])
|
1283
|
+
|
1284
|
+
#adjust_arrays()
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
#
|
1288
|
+
# Returns true if the given time matches this cron line.
|
1289
|
+
#
|
1290
|
+
# (the precision is passed as well to determine if it's
|
1291
|
+
# worth checking seconds and minutes)
|
1292
|
+
#
|
1293
|
+
def matches? (time)
|
1294
|
+
#def matches? (time, precision)
|
1295
|
+
|
1296
|
+
time = Time.at(time) \
|
1297
|
+
if time.kind_of?(Float) or time.kind_of?(Integer)
|
1298
|
+
|
1299
|
+
return false \
|
1300
|
+
if no_match?(time.sec, @seconds)
|
1301
|
+
#if precision <= 1 and no_match?(time.sec, @seconds)
|
1302
|
+
return false \
|
1303
|
+
if no_match?(time.min, @minutes)
|
1304
|
+
#if precision <= 60 and no_match?(time.min, @minutes)
|
1305
|
+
return false \
|
1306
|
+
if no_match?(time.hour, @hours)
|
1307
|
+
return false \
|
1308
|
+
if no_match?(time.day, @days)
|
1309
|
+
return false \
|
1310
|
+
if no_match?(time.month, @months)
|
1311
|
+
return false \
|
1312
|
+
if no_match?(time.wday, @weekdays)
|
1313
|
+
|
1314
|
+
true
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
#
|
1318
|
+
# Returns an array of 6 arrays (seconds, minutes, hours, days,
|
1319
|
+
# months, weekdays).
|
1320
|
+
# This method is used by the cronline unit tests.
|
1321
|
+
#
|
1322
|
+
def to_array
|
1323
|
+
[ @seconds, @minutes, @hours, @days, @months, @weekdays ]
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
private
|
1326
1327
|
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1328
|
+
#--
|
1329
|
+
# adjust values to Ruby
|
1330
|
+
#
|
1331
|
+
#def adjust_arrays()
|
1332
|
+
# @hours = @hours.collect { |h|
|
1333
|
+
# if h == 24
|
1334
|
+
# 0
|
1335
|
+
# else
|
1336
|
+
# h
|
1337
|
+
# end
|
1338
|
+
# } if @hours
|
1339
|
+
# @weekdays = @weekdays.collect { |wd|
|
1340
|
+
# wd - 1
|
1341
|
+
# } if @weekdays
|
1342
|
+
#end
|
1343
|
+
#
|
1344
|
+
# dead code, keeping it as a reminder
|
1345
|
+
#++
|
1330
1346
|
|
1331
|
-
|
1347
|
+
WDS = [ "sun", "mon", "tue", "wed", "thu", "fri", "sat" ]
|
1348
|
+
#
|
1349
|
+
# used by parse_weekday()
|
1332
1350
|
|
1333
|
-
|
1351
|
+
def parse_weekdays (item)
|
1334
1352
|
|
1335
|
-
|
1336
|
-
item = item.gsub(day, "#{index+1}")
|
1337
|
-
end
|
1353
|
+
item = item.downcase
|
1338
1354
|
|
1339
|
-
|
1340
|
-
|
1355
|
+
WDS.each_with_index do |day, index|
|
1356
|
+
item = item.gsub day, "#{index}"
|
1357
|
+
end
|
1341
1358
|
|
1342
|
-
|
1359
|
+
r = parse_item item, 0, 7
|
1343
1360
|
|
1344
|
-
|
1345
|
-
if item == "*"
|
1346
|
-
return parse_list(item, min, max) \
|
1347
|
-
if item.index(",")
|
1348
|
-
return parse_range(item, min, max) \
|
1349
|
-
if item.index("*") or item.index("-")
|
1361
|
+
return r unless r.is_a?(Array)
|
1350
1362
|
|
1351
|
-
|
1363
|
+
r.collect { |e| e == 7 ? 0 : e }.uniq
|
1364
|
+
end
|
1352
1365
|
|
1353
|
-
|
1354
|
-
i = max if i > max
|
1366
|
+
def parse_item (item, min, max)
|
1355
1367
|
|
1356
|
-
|
1357
|
-
|
1368
|
+
return nil \
|
1369
|
+
if item == "*"
|
1370
|
+
return parse_list(item, min, max) \
|
1371
|
+
if item.index(",")
|
1372
|
+
return parse_range(item, min, max) \
|
1373
|
+
if item.index("*") or item.index("-")
|
1358
1374
|
|
1359
|
-
|
1375
|
+
i = Integer(item)
|
1360
1376
|
|
1361
|
-
|
1377
|
+
i = min if i < min
|
1378
|
+
i = max if i > max
|
1362
1379
|
|
1363
|
-
|
1380
|
+
[ i ]
|
1381
|
+
end
|
1364
1382
|
|
1365
|
-
|
1383
|
+
def parse_list (item, min, max)
|
1366
1384
|
|
1367
|
-
|
1368
|
-
i = max if i > max
|
1385
|
+
items = item.split(",")
|
1369
1386
|
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1387
|
+
items.inject([]) { |r, i| r.push(parse_range(i, min, max)) }.flatten
|
1388
|
+
end
|
1389
|
+
|
1390
|
+
def parse_range (item, min, max)
|
1373
1391
|
|
1374
|
-
|
1392
|
+
i = item.index("-")
|
1393
|
+
j = item.index("/")
|
1375
1394
|
|
1376
|
-
|
1377
|
-
j = item.index("/")
|
1395
|
+
return item.to_i if (not i and not j)
|
1378
1396
|
|
1379
|
-
|
1397
|
+
inc = 1
|
1380
1398
|
|
1381
|
-
|
1399
|
+
inc = Integer(item[j+1..-1]) if j
|
1382
1400
|
|
1383
|
-
|
1384
|
-
|
1401
|
+
istart = -1
|
1402
|
+
iend = -1
|
1385
1403
|
|
1386
|
-
|
1404
|
+
if i
|
1387
1405
|
|
1388
|
-
|
1406
|
+
istart = Integer(item[0..i-1])
|
1389
1407
|
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
|
1408
|
+
if j
|
1409
|
+
iend = Integer(item[i+1..j])
|
1410
|
+
else
|
1411
|
+
iend = Integer(item[i+1..-1])
|
1412
|
+
end
|
1395
1413
|
|
1396
|
-
|
1414
|
+
else # case */x
|
1397
1415
|
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1416
|
+
istart = min
|
1417
|
+
iend = max
|
1418
|
+
end
|
1401
1419
|
|
1402
|
-
|
1403
|
-
|
1420
|
+
istart = min if istart < min
|
1421
|
+
iend = max if iend > max
|
1404
1422
|
|
1405
|
-
|
1423
|
+
result = []
|
1406
1424
|
|
1407
|
-
|
1408
|
-
|
1425
|
+
value = istart
|
1426
|
+
loop do
|
1409
1427
|
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1428
|
+
result << value
|
1429
|
+
value = value + inc
|
1430
|
+
break if value > iend
|
1431
|
+
end
|
1414
1432
|
|
1415
|
-
|
1416
|
-
|
1433
|
+
result
|
1434
|
+
end
|
1417
1435
|
|
1418
|
-
|
1436
|
+
def no_match? (value, cron_values)
|
1419
1437
|
|
1420
|
-
|
1438
|
+
return false if not cron_values
|
1421
1439
|
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1440
|
+
cron_values.each do |v|
|
1441
|
+
return false if value == v # ok, it matches
|
1442
|
+
end
|
1425
1443
|
|
1426
|
-
|
1427
|
-
end
|
1444
|
+
true # no match found
|
1428
1445
|
end
|
1446
|
+
end
|
1429
1447
|
|
1430
1448
|
end
|
1431
1449
|
|