rufus-scheduler 1.0.12 → 1.0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,339 @@
1
+ #
2
+ #--
3
+ # Copyright (c) 2006-2009, John Mettraux, jmettraux@gmail.com
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+ #++
23
+ #
24
+
25
+ #
26
+ # "made in Japan"
27
+ #
28
+ # John Mettraux at openwfe.org
29
+ #
30
+
31
+ module Rufus
32
+
33
+ JOB_ID_LOCK = Mutex.new
34
+
35
+ #
36
+ # The parent class for scheduled jobs.
37
+ #
38
+ class Job
39
+
40
+ @@last_given_id = 0
41
+ #
42
+ # as a scheduler is fully transient, no need to
43
+ # have persistent ids, a simple counter is sufficient
44
+
45
+ #
46
+ # The identifier for the job
47
+ #
48
+ attr_accessor :job_id
49
+
50
+ #
51
+ # An array of tags
52
+ #
53
+ attr_accessor :tags
54
+
55
+ #
56
+ # The block to execute at trigger time
57
+ #
58
+ attr_accessor :block
59
+
60
+ #
61
+ # A reference to the scheduler
62
+ #
63
+ attr_reader :scheduler
64
+
65
+ #
66
+ # Keeping a copy of the initialization params of the job.
67
+ #
68
+ attr_reader :params
69
+
70
+ #
71
+ # if the job is currently executing, this field points to
72
+ # the 'trigger thread'
73
+ #
74
+ attr_reader :trigger_thread
75
+
76
+
77
+ def initialize (scheduler, job_id, params, &block)
78
+
79
+ @scheduler = scheduler
80
+ @block = block
81
+
82
+ if job_id
83
+ @job_id = job_id
84
+ else
85
+ JOB_ID_LOCK.synchronize do
86
+ @job_id = @@last_given_id
87
+ @@last_given_id = @job_id + 1
88
+ end
89
+ end
90
+
91
+ @params = params
92
+
93
+ #@tags = Array(tags).collect { |tag| tag.to_s }
94
+ # making sure we have an array of String tags
95
+
96
+ @tags = Array(params[:tags])
97
+ # any tag is OK
98
+ end
99
+
100
+ #
101
+ # Returns true if this job sports the given tag
102
+ #
103
+ def has_tag? (tag)
104
+
105
+ @tags.include?(tag)
106
+ end
107
+
108
+ #
109
+ # Removes (cancels) this job from its scheduler.
110
+ #
111
+ def unschedule
112
+
113
+ @scheduler.unschedule(@job_id)
114
+ end
115
+
116
+ #
117
+ # Triggers the job (in a dedicated thread).
118
+ #
119
+ def trigger
120
+
121
+ t = Thread.new do
122
+
123
+ @trigger_thread = Thread.current
124
+ # keeping track of the thread
125
+
126
+ begin
127
+
128
+ do_trigger
129
+
130
+ rescue Exception => e
131
+
132
+ @scheduler.send(:log_exception, e)
133
+ end
134
+
135
+ #@trigger_thread = nil if @trigger_thread == Thread.current
136
+ @trigger_thread = nil
137
+ # overlapping executions, what to do ?
138
+ end
139
+
140
+ if t.alive? and (to = @params[:timeout])
141
+ @scheduler.in(to, :tags => 'timeout') do
142
+ @trigger_thread.raise(Rufus::TimeOutError) if t.alive?
143
+ end
144
+ end
145
+ end
146
+
147
+ def call_block
148
+
149
+ args = case @block.arity
150
+ when 0 then []
151
+ when 1 then [ @params ]
152
+ when 2 then [ @job_id, @params ]
153
+ else [ @job_id, schedule_info, @params ]
154
+ end
155
+
156
+ @block.call(*args)
157
+ end
158
+ end
159
+
160
+ #
161
+ # An 'at' job.
162
+ #
163
+ class AtJob < Job
164
+
165
+ #
166
+ # The float representation (Time.to_f) of the time at which
167
+ # the job should be triggered.
168
+ #
169
+ attr_accessor :at
170
+
171
+
172
+ def initialize (scheduler, at, at_id, params, &block)
173
+
174
+ super(scheduler, at_id, params, &block)
175
+ @at = at
176
+ end
177
+
178
+ #
179
+ # Returns the Time instance at which this job is scheduled.
180
+ #
181
+ def schedule_info
182
+
183
+ Time.at(@at)
184
+ end
185
+
186
+ #
187
+ # next_time is last_time (except for EveryJob instances). Returns
188
+ # a Time instance.
189
+ #
190
+ def next_time
191
+
192
+ schedule_info
193
+ end
194
+
195
+ protected
196
+
197
+ #
198
+ # Triggers the job (calls the block)
199
+ #
200
+ def do_trigger
201
+
202
+ call_block
203
+
204
+ @scheduler.instance_variable_get(:@non_cron_jobs).delete(@job_id)
205
+ end
206
+ end
207
+
208
+ #
209
+ # An 'every' job is simply an extension of an 'at' job.
210
+ #
211
+ class EveryJob < AtJob
212
+
213
+ #
214
+ # Returns the frequency string used to schedule this EveryJob,
215
+ # like for example "3d" or "1M10d3h".
216
+ #
217
+ def schedule_info
218
+
219
+ @params[:every]
220
+ end
221
+
222
+ protected
223
+
224
+ #
225
+ # triggers the job, then reschedules it if necessary
226
+ #
227
+ def do_trigger
228
+
229
+ hit_exception = false
230
+
231
+ begin
232
+
233
+ call_block
234
+
235
+ rescue Exception => e
236
+
237
+ @scheduler.send(:log_exception, e)
238
+
239
+ hit_exception = true
240
+ end
241
+
242
+ if \
243
+ @scheduler.instance_variable_get(:@exit_when_no_more_jobs) or
244
+ (@params[:dont_reschedule] == true) or
245
+ (hit_exception and @params[:try_again] == false)
246
+
247
+ @scheduler.instance_variable_get(:@non_cron_jobs).delete(job_id)
248
+ # maybe it'd be better to wipe that reference from here anyway...
249
+
250
+ return
251
+ end
252
+
253
+ #
254
+ # ok, reschedule ...
255
+
256
+ params[:job] = self
257
+
258
+ @at = @at + Rufus.duration_to_f(params[:every])
259
+
260
+ @scheduler.send(:do_schedule_at, @at, params)
261
+ end
262
+ end
263
+
264
+ #
265
+ # A cron job.
266
+ #
267
+ class CronJob < Job
268
+
269
+ #
270
+ # The CronLine instance representing the times at which
271
+ # the cron job has to be triggered.
272
+ #
273
+ attr_accessor :cron_line
274
+
275
+ def initialize (scheduler, cron_id, line, params, &block)
276
+
277
+ super(scheduler, cron_id, params, &block)
278
+
279
+ if line.is_a?(String)
280
+
281
+ @cron_line = CronLine.new(line)
282
+
283
+ elsif line.is_a?(CronLine)
284
+
285
+ @cron_line = line
286
+
287
+ else
288
+
289
+ raise(
290
+ "Cannot initialize a CronJob " +
291
+ "with a param of class #{line.class}")
292
+ end
293
+ end
294
+
295
+ #
296
+ # This is the method called by the scheduler to determine if it
297
+ # has to fire this CronJob instance.
298
+ #
299
+ def matches? (time)
300
+ #def matches? (time, precision)
301
+
302
+ #@cron_line.matches?(time, precision)
303
+ @cron_line.matches?(time)
304
+ end
305
+
306
+ #
307
+ # Returns the original cron tab string used to schedule this
308
+ # Job. Like for example "60/3 * * * Sun".
309
+ #
310
+ def schedule_info
311
+
312
+ @cron_line.original
313
+ end
314
+
315
+ #
316
+ # Returns a Time instance : the next time this cron job is
317
+ # supposed to "fire".
318
+ #
319
+ # 'from' is used to specify the starting point for determining
320
+ # what will be the next time. Defaults to now.
321
+ #
322
+ def next_time (from=Time.now)
323
+
324
+ @cron_line.next_time(from)
325
+ end
326
+
327
+ protected
328
+
329
+ #
330
+ # As the name implies.
331
+ #
332
+ def do_trigger
333
+
334
+ call_block
335
+ end
336
+ end
337
+
338
+ end
339
+
@@ -0,0 +1,375 @@
1
+ #
2
+ #--
3
+ # Copyright (c) 2005-2009, John Mettraux, jmettraux@gmail.com
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+ #++
23
+ #
24
+
25
+ #
26
+ # "hecho en Costa Rica"
27
+ #
28
+ # john.mettraux@openwfe.org
29
+ #
30
+
31
+ require 'date'
32
+ #require 'parsedate'
33
+
34
+
35
+ module Rufus
36
+
37
+ #TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
38
+
39
+ #
40
+ # Returns the current time as an ISO date string
41
+ #
42
+ def Rufus.now
43
+
44
+ to_iso8601_date(Time.new())
45
+ end
46
+
47
+ #
48
+ # As the name implies.
49
+ #
50
+ def Rufus.to_iso8601_date (date)
51
+
52
+ if date.kind_of? Float
53
+ date = to_datetime(Time.at(date))
54
+ elsif date.kind_of? Time
55
+ date = to_datetime(date)
56
+ elsif not date.kind_of? Date
57
+ date = DateTime.parse(date)
58
+ end
59
+
60
+ s = date.to_s # this is costly
61
+ s[10] = ' '
62
+
63
+ s
64
+ end
65
+
66
+ #
67
+ # the old method we used to generate our ISO datetime strings
68
+ #
69
+ def Rufus.time_to_iso8601_date (time)
70
+
71
+ s = time.getutc().strftime(TIME_FORMAT)
72
+ o = time.utc_offset / 3600
73
+ o = o.to_s + '00'
74
+ o = '0' + o if o.length < 4
75
+ o = '+' + o unless o[0..1] == '-'
76
+
77
+ s + ' ' + o.to_s
78
+ end
79
+
80
+ #
81
+ # Returns a Ruby time
82
+ #
83
+ def Rufus.to_ruby_time (iso_date)
84
+
85
+ DateTime.parse(iso_date)
86
+ end
87
+
88
+ #def Rufus.parse_date (date)
89
+ #end
90
+
91
+ #
92
+ # equivalent to java.lang.System.currentTimeMillis()
93
+ #
94
+ def Rufus.current_time_millis
95
+
96
+ (Time.new.to_f * 1000).to_i
97
+ end
98
+
99
+ #
100
+ # Turns a string like '1m10s' into a float like '70.0', more formally,
101
+ # turns a time duration expressed as a string into a Float instance
102
+ # (millisecond count).
103
+ #
104
+ # w -> week
105
+ # d -> day
106
+ # h -> hour
107
+ # m -> minute
108
+ # s -> second
109
+ # M -> month
110
+ # y -> year
111
+ # 'nada' -> millisecond
112
+ #
113
+ # Some examples :
114
+ #
115
+ # Rufus.parse_time_string "500" # => 0.5
116
+ # Rufus.parse_time_string "1000" # => 1.0
117
+ # Rufus.parse_time_string "1h" # => 3600.0
118
+ # Rufus.parse_time_string "1h10s" # => 3610.0
119
+ # Rufus.parse_time_string "1w2d" # => 777600.0
120
+ #
121
+ def Rufus.parse_time_string (string)
122
+
123
+ string = string.strip
124
+
125
+ index = -1
126
+ result = 0.0
127
+
128
+ number = ''
129
+
130
+ loop do
131
+
132
+ index = index + 1
133
+
134
+ if index >= string.length
135
+ result = result + (Float(number) / 1000.0) if number.length > 0
136
+ break
137
+ end
138
+
139
+ c = string[index, 1]
140
+
141
+ #if is_digit?(c)
142
+ if (c >= '0' and c <= '9')
143
+ number = number + c
144
+ next
145
+ end
146
+
147
+ value = Integer(number)
148
+ number = ''
149
+
150
+ multiplier = DURATIONS[c]
151
+
152
+ raise "unknown time char '#{c}'" \
153
+ if not multiplier
154
+
155
+ result = result + (value * multiplier)
156
+ end
157
+
158
+ result
159
+ end
160
+
161
+ class << self
162
+ alias_method :parse_duration_string, :parse_time_string
163
+ end
164
+
165
+ #
166
+ # Returns true if the character c is a digit
167
+ #
168
+ # (probably better served by a regex)
169
+ #
170
+ def Rufus.is_digit? (c)
171
+
172
+ return false if not c.kind_of?(String)
173
+ return false if c.length > 1
174
+ (c >= '0' and c <= '9')
175
+ end
176
+
177
+ #
178
+ # conversion methods between Date[Time] and Time
179
+
180
+ #
181
+ # Ruby Cookbook 1st edition p.111
182
+ # http://www.oreilly.com/catalog/rubyckbk/
183
+ # a must
184
+ #
185
+
186
+ #
187
+ # converts a Time instance to a DateTime one
188
+ #
189
+ def Rufus.to_datetime (time)
190
+
191
+ s = time.sec + Rational(time.usec, 10**6)
192
+ o = Rational(time.utc_offset, 3600 * 24)
193
+
194
+ begin
195
+
196
+ DateTime.new(
197
+ time.year,
198
+ time.month,
199
+ time.day,
200
+ time.hour,
201
+ time.min,
202
+ s,
203
+ o)
204
+
205
+ rescue Exception => e
206
+
207
+ DateTime.new(
208
+ time.year,
209
+ time.month,
210
+ time.day,
211
+ time.hour,
212
+ time.min,
213
+ time.sec,
214
+ time.utc_offset)
215
+ end
216
+ end
217
+
218
+ def Rufus.to_gm_time (dtime)
219
+
220
+ to_ttime(dtime.new_offset, :gm)
221
+ end
222
+
223
+ def Rufus.to_local_time (dtime)
224
+
225
+ to_ttime(dtime.new_offset(DateTime.now.offset-offset), :local)
226
+ end
227
+
228
+ def Rufus.to_ttime (d, method)
229
+
230
+ usec = (d.sec_fraction * 3600 * 24 * (10**6)).to_i
231
+ Time.send(method, d.year, d.month, d.day, d.hour, d.min, d.sec, usec)
232
+ end
233
+
234
+ #
235
+ # Turns a number of seconds into a a time string
236
+ #
237
+ # Rufus.to_duration_string 0 # => '0s'
238
+ # Rufus.to_duration_string 60 # => '1m'
239
+ # Rufus.to_duration_string 3661 # => '1h1m1s'
240
+ # Rufus.to_duration_string 7 * 24 * 3600 # => '1w'
241
+ # Rufus.to_duration_string 30 * 24 * 3600 + 1 # => "4w2d1s"
242
+ #
243
+ # It goes from seconds to the year. Months are not counted (as they
244
+ # are of variable length). Weeks are counted.
245
+ #
246
+ # For 30 days months to be counted, the second parameter of this
247
+ # method can be set to true.
248
+ #
249
+ # Rufus.to_time_string 30 * 24 * 3600 + 1, true # => "1M1s"
250
+ #
251
+ # (to_time_string is an alias for to_duration_string)
252
+ #
253
+ # If a Float value is passed, milliseconds will be displayed without
254
+ # 'marker'
255
+ #
256
+ # Rufus.to_duration_string 0.051 # =>"51"
257
+ # Rufus.to_duration_string 7.051 # =>"7s51"
258
+ # Rufus.to_duration_string 0.120 + 30 * 24 * 3600 + 1 # =>"4w2d1s120"
259
+ #
260
+ # (this behaviour mirrors the one found for parse_time_string()).
261
+ #
262
+ # Options are :
263
+ #
264
+ # * :months, if set to true, months (M) of 30 days will be taken into
265
+ # account when building up the result
266
+ # * :drop_seconds, if set to true, seconds and milliseconds will be trimmed
267
+ # from the result
268
+ #
269
+ def Rufus.to_duration_string (seconds, options={})
270
+
271
+ return (options[:drop_seconds] ? '0m' : '0s') if seconds <= 0
272
+
273
+ h = to_duration_hash seconds, options
274
+
275
+ s = DU_KEYS.inject('') do |r, key|
276
+ count = h[key]
277
+ count = nil if count == 0
278
+ r << "#{count}#{key}" if count
279
+ r
280
+ end
281
+
282
+ ms = h[:ms]
283
+ s << ms.to_s if ms
284
+
285
+ s
286
+ end
287
+
288
+ class << self
289
+ alias_method :to_time_string, :to_duration_string
290
+ end
291
+
292
+ #
293
+ # Turns a number of seconds (integer or Float) into a hash like in :
294
+ #
295
+ # Rufus.to_duration_hash 0.051
296
+ # # => { :ms => "51" }
297
+ # Rufus.to_duration_hash 7.051
298
+ # # => { :s => 7, :ms => "51" }
299
+ # Rufus.to_duration_hash 0.120 + 30 * 24 * 3600 + 1
300
+ # # => { :w => 4, :d => 2, :s => 1, :ms => "120" }
301
+ #
302
+ # This method is used by to_duration_string (to_time_string) behind
303
+ # the scene.
304
+ #
305
+ # Options are :
306
+ #
307
+ # * :months, if set to true, months (M) of 30 days will be taken into
308
+ # account when building up the result
309
+ # * :drop_seconds, if set to true, seconds and milliseconds will be trimmed
310
+ # from the result
311
+ #
312
+ def Rufus.to_duration_hash (seconds, options={})
313
+
314
+ h = {}
315
+
316
+ if seconds.is_a?(Float)
317
+ h[:ms] = (seconds % 1 * 1000).to_i
318
+ seconds = seconds.to_i
319
+ end
320
+
321
+ if options[:drop_seconds]
322
+ h.delete :ms
323
+ seconds = (seconds - seconds % 60)
324
+ end
325
+
326
+ durations = options[:months] ? DURATIONS2M : DURATIONS2
327
+
328
+ durations.each do |key, duration|
329
+
330
+ count = seconds / duration
331
+ seconds = seconds % duration
332
+
333
+ h[key.to_sym] = count if count > 0
334
+ end
335
+
336
+ h
337
+ end
338
+
339
+ #
340
+ # Ensures that a duration is a expressed as a Float instance.
341
+ #
342
+ # duration_to_f("10s")
343
+ #
344
+ # will yield 10.0
345
+ #
346
+ def Rufus.duration_to_f (s)
347
+
348
+ return s if s.kind_of?(Float)
349
+ return parse_time_string(s) if s.kind_of?(String)
350
+ Float(s.to_s)
351
+ end
352
+
353
+ protected
354
+
355
+ DURATIONS2M = [
356
+ [ 'y', 365 * 24 * 3600 ],
357
+ [ 'M', 30 * 24 * 3600 ],
358
+ [ 'w', 7 * 24 * 3600 ],
359
+ [ 'd', 24 * 3600 ],
360
+ [ 'h', 3600 ],
361
+ [ 'm', 60 ],
362
+ [ 's', 1 ]
363
+ ]
364
+ DURATIONS2 = DURATIONS2M.dup
365
+ DURATIONS2.delete_at(1)
366
+
367
+ DURATIONS = DURATIONS2M.inject({}) do |r, (k, v)|
368
+ r[k] = v
369
+ r
370
+ end
371
+
372
+ DU_KEYS = DURATIONS2M.collect { |k, v| k.to_sym }
373
+
374
+ end
375
+