rufus-scheduler 1.0.12 → 1.0.13

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