rufus-scheduler 1.0.14 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+
2
+ [o] spec for jobs in the past (in and at)
3
+ [o] :discard_past
4
+
5
+ [o] every
6
+ [o] cron
7
+
8
+ [o] CHECK every and unschedule !!!
9
+
10
+ [o] :tags
11
+ [o] timeout feature (at/in/every/cron) in Job class
12
+
13
+ [o] :first_in, :first_at
14
+
15
+ [x] :dont_reschedule (or block returns false ?)
16
+
17
+ [o] [get_]jobs methods
18
+ [o] find methods
19
+
20
+ [x] CTRL-C during tests : allow, trap_int...
21
+
22
+ [o] 1.9
23
+ [o] j1.2.0
24
+
25
+ [o] revise trigger block arity
26
+ use a compatibility switch ? yes
27
+
28
+ [o] synchronize @cron_jobs ?
29
+
30
+ [o] why not : make it work even if EM is not present
31
+ EmScheduler < Scheduler
32
+ FiberScheduler < Scheduler
33
+
34
+ [x] :blocking => 'blockname' idea, mutex = @mutexes['blockname'] ...
35
+ [o] eventually, make sleep frequency customizable
36
+
37
+ [o] PlainScheduler : name thread
38
+
39
+ [o] document :blocking
40
+
41
+ [o] README.rdoc
42
+ [o] fix jruby120 --em
43
+
44
+ [o] handle_exception (job, e)
45
+
46
+ [o] Schedulable
47
+
48
+ [o] Rufus::Scheduler.start_new() : autodetect EM ?
49
+ [o] check :blocking and every (reschedule blocking...)
50
+ [o] document :thread_name scheduler option
51
+
@@ -1,3 +1,3 @@
1
1
 
2
- require 'rufus/scheduler/scheduler'
2
+ require 'rufus/sc/scheduler'
3
3
 
@@ -1,3 +1,3 @@
1
1
 
2
- require 'rufus/scheduler/otime'
2
+ require 'rufus/sc/rtime.rb'
3
3
 
@@ -52,45 +52,34 @@ module Rufus
52
52
 
53
53
  items = line.split
54
54
 
55
- unless [ 5, 6 ].include?(items.length)
56
- raise \
57
- "cron '#{line}' string should hold 5 or 6 items, " +
58
- "not #{items.length}" \
55
+ unless items.length == 5 or items.length == 6
56
+ raise(
57
+ "cron '#{line}' string should hold 5 or 6 items, not #{items.length}")
59
58
  end
60
59
 
61
60
  offset = items.length - 5
62
61
 
63
- @seconds = if offset == 1
64
- parse_item(items[0], 0, 59)
65
- else
66
- [ 0 ]
67
- end
68
- @minutes = parse_item(items[0+offset], 0, 59)
69
- @hours = parse_item(items[1+offset], 0, 24)
70
- @days = parse_item(items[2+offset], 1, 31)
71
- @months = parse_item(items[3+offset], 1, 12)
72
- @weekdays = parse_weekdays(items[4+offset])
73
-
74
- #adjust_arrays()
62
+ @seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
63
+ @minutes = parse_item(items[0 + offset], 0, 59)
64
+ @hours = parse_item(items[1 + offset], 0, 24)
65
+ @days = parse_item(items[2 + offset], 1, 31)
66
+ @months = parse_item(items[3 + offset], 1, 12)
67
+ @weekdays = parse_weekdays(items[4 + offset])
75
68
  end
76
69
 
77
70
  #
78
71
  # Returns true if the given time matches this cron line.
79
72
  #
80
- # (the precision is passed as well to determine if it's
81
- # worth checking seconds and minutes)
82
- #
83
73
  def matches? (time)
84
- #def matches? (time, precision)
85
74
 
86
75
  time = Time.at(time) unless time.kind_of?(Time)
87
76
 
88
- return false unless sub_match? time.sec, @seconds
89
- return false unless sub_match? time.min, @minutes
90
- return false unless sub_match? time.hour, @hours
91
- return false unless sub_match? time.day, @days
92
- return false unless sub_match? time.month, @months
93
- return false unless sub_match? time.wday, @weekdays
77
+ return false unless sub_match?(time.sec, @seconds)
78
+ return false unless sub_match?(time.min, @minutes)
79
+ return false unless sub_match?(time.hour, @hours)
80
+ return false unless sub_match?(time.day, @days)
81
+ return false unless sub_match?(time.month, @months)
82
+ return false unless sub_match?(time.wday, @weekdays)
94
83
  true
95
84
  end
96
85
 
@@ -128,27 +117,29 @@ module Rufus
128
117
  #
129
118
  # (Thanks to K Liu for the note and the examples)
130
119
  #
131
- def next_time time=Time.now
120
+ def next_time (time=Time.now)
121
+
132
122
  time -= time.usec * 1e-6
133
123
  time += 1
134
124
 
135
125
  loop do
136
- unless date_match? time
126
+
127
+ unless date_match?(time)
137
128
  time += (24 - time.hour) * 3600 - time.min * 60 - time.sec
138
129
  next
139
130
  end
140
131
 
141
- unless sub_match? time.hour, @hours
132
+ unless sub_match?(time.hour, @hours)
142
133
  time += (60 - time.min) * 60 - time.sec
143
134
  next
144
135
  end
145
136
 
146
- unless sub_match? time.min, @minutes
137
+ unless sub_match?(time.min, @minutes)
147
138
  time += 60 - time.sec
148
139
  next
149
140
  end
150
141
 
151
- unless sub_match? time.sec, @seconds
142
+ unless sub_match?(time.sec, @seconds)
152
143
  time += 1
153
144
  next
154
145
  end
@@ -161,25 +152,6 @@ module Rufus
161
152
 
162
153
  private
163
154
 
164
- #--
165
- # adjust values to Ruby
166
- #
167
- #def adjust_arrays()
168
- # @hours = @hours.collect { |h|
169
- # if h == 24
170
- # 0
171
- # else
172
- # h
173
- # end
174
- # } if @hours
175
- # @weekdays = @weekdays.collect { |wd|
176
- # wd - 1
177
- # } if @weekdays
178
- #end
179
- #
180
- # dead code, keeping it as a reminder
181
- #++
182
-
183
155
  WDS = %w[ sun mon tue wed thu fri sat ]
184
156
  #
185
157
  # used by parse_weekday()
@@ -188,25 +160,20 @@ module Rufus
188
160
 
189
161
  item = item.downcase
190
162
 
191
- WDS.each_with_index do |day, index|
192
- item = item.gsub day, "#{index}"
193
- end
194
-
195
- r = parse_item item, 0, 7
163
+ WDS.each_with_index { |day, index| item = item.gsub(day, index.to_s) }
196
164
 
197
- return r unless r.is_a?(Array)
165
+ r = parse_item(item, 0, 7)
198
166
 
199
- r.collect { |e| e == 7 ? 0 : e }.uniq
167
+ r.is_a?(Array) ?
168
+ r.collect { |e| e == 7 ? 0 : e }.uniq :
169
+ r
200
170
  end
201
171
 
202
172
  def parse_item (item, min, max)
203
173
 
204
- return nil \
205
- if item == "*"
206
- return parse_list(item, min, max) \
207
- if item.index(",")
208
- return parse_range(item, min, max) \
209
- if item.index("*") or item.index("-")
174
+ return nil if item == '*'
175
+ return parse_list(item, min, max) if item.index(',')
176
+ return parse_range(item, min, max) if item.index('*') or item.index('-')
210
177
 
211
178
  i = Integer(item)
212
179
 
@@ -218,33 +185,31 @@ module Rufus
218
185
 
219
186
  def parse_list (item, min, max)
220
187
 
221
- items = item.split(",")
222
-
223
- items.inject([]) { |r, i| r.push(parse_range(i, min, max)) }.flatten
188
+ item.split(',').inject([]) { |r, i|
189
+ r.push(parse_range(i, min, max))
190
+ }.flatten
224
191
  end
225
192
 
226
193
  def parse_range (item, min, max)
227
194
 
228
- i = item.index("-")
229
- j = item.index("/")
195
+ i = item.index('-')
196
+ j = item.index('/')
230
197
 
231
198
  return item.to_i if (not i and not j)
232
199
 
233
- inc = 1
234
-
235
- inc = Integer(item[j+1..-1]) if j
200
+ inc = j ? Integer(item[j+1..-1]) : 1
236
201
 
237
202
  istart = -1
238
203
  iend = -1
239
204
 
240
205
  if i
241
206
 
242
- istart = Integer(item[0..i-1])
207
+ istart = Integer(item[0..i - 1])
243
208
 
244
209
  if j
245
- iend = Integer(item[i+1..j])
210
+ iend = Integer(item[i + 1..j])
246
211
  else
247
- iend = Integer(item[i+1..-1])
212
+ iend = Integer(item[i + 1..-1])
248
213
  end
249
214
 
250
215
  else # case */x
@@ -260,7 +225,6 @@ module Rufus
260
225
 
261
226
  value = istart
262
227
  loop do
263
-
264
228
  result << value
265
229
  value = value + inc
266
230
  break if value > iend
@@ -269,14 +233,14 @@ module Rufus
269
233
  result
270
234
  end
271
235
 
272
- def sub_match? value, values
236
+ def sub_match?(value, values)
273
237
  values.nil? || values.include?(value)
274
238
  end
275
239
 
276
- def date_match? date
277
- return false unless sub_match? date.day, @days
278
- return false unless sub_match? date.month, @months
279
- return false unless sub_match? date.wday, @weekdays
240
+ def date_match?(date)
241
+ return false unless sub_match?(date.day, @days)
242
+ return false unless sub_match?(date.month, @months)
243
+ return false unless sub_match?(date.wday, @weekdays)
280
244
  true
281
245
  end
282
246
  end
@@ -0,0 +1,157 @@
1
+ #--
2
+ # Copyright (c) 2006-2009, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+
26
+ require 'thread'
27
+
28
+
29
+ module Rufus
30
+ module Scheduler
31
+
32
+ #
33
+ # Tracking at/in/every jobs.
34
+ #
35
+ # In order of trigger time.
36
+ #
37
+ class JobQueue
38
+
39
+ # Mapping :at|:in|:every to their respective job classes.
40
+ #
41
+ JOB_TYPES = {
42
+ :at => Rufus::Scheduler::AtJob,
43
+ :in => Rufus::Scheduler::InJob,
44
+ :every => Rufus::Scheduler::EveryJob
45
+ }
46
+
47
+ def initialize
48
+
49
+ @mutex = Mutex.new
50
+ @jobs = []
51
+ end
52
+
53
+ # Returns the next job to trigger. Returns nil if none eligible.
54
+ #
55
+ def job_to_trigger
56
+
57
+ @mutex.synchronize do
58
+ if @jobs.size > 0 && Time.now.to_f >= @jobs.first.at
59
+ @jobs.shift
60
+ else
61
+ nil
62
+ end
63
+ end
64
+ end
65
+
66
+ # Adds this job to the map.
67
+ #
68
+ def << (job)
69
+
70
+ @mutex.synchronize do
71
+ delete(job.job_id)
72
+ @jobs << job
73
+ @jobs.sort! { |j0, j1| j0.at <=> j1.at }
74
+ end
75
+ end
76
+
77
+ # Removes a job (given its id). Returns nil if the job was not found.
78
+ #
79
+ def unschedule (job_id)
80
+
81
+ @mutex.synchronize { delete(job_id) }
82
+ end
83
+
84
+ # Returns a mapping job_id => job
85
+ #
86
+ def to_h
87
+
88
+ @jobs.inject({}) { |h, j| h[j.job_id] = j; h }
89
+ end
90
+
91
+ # Returns a list of jobs of the given type (:at|:in|:every)
92
+ #
93
+ def select (type)
94
+
95
+ type = JOB_TYPES[type]
96
+ @jobs.select { |j| j.is_a?(type) }
97
+ end
98
+
99
+ def size
100
+
101
+ @jobs.size
102
+ end
103
+
104
+ protected
105
+
106
+ def delete (job_id)
107
+ j = @jobs.find { |j| j.job_id == job_id }
108
+ @jobs.delete(j) if j
109
+ j
110
+ end
111
+ end
112
+
113
+ #
114
+ # Tracking cron jobs.
115
+ #
116
+ # (mostly synchronizing access to the map of cron jobs)
117
+ #
118
+ class CronJobQueue
119
+
120
+ def initialize
121
+
122
+ @mutex = Mutex.new
123
+ @jobs = {}
124
+ end
125
+
126
+ def unschedule (job_id)
127
+
128
+ @mutex.synchronize { @jobs.delete(job_id) }
129
+ end
130
+
131
+ def trigger_matching_jobs (now)
132
+
133
+ js = @mutex.synchronize { @jobs.values }
134
+ # maybe this sync is a bit paranoid
135
+
136
+ js.each { |job| job.trigger_if_matches(now) }
137
+ end
138
+
139
+ def << (job)
140
+
141
+ @mutex.synchronize { @jobs[job.job_id] = job }
142
+ end
143
+
144
+ def size
145
+
146
+ @jobs.size
147
+ end
148
+
149
+ def to_h
150
+
151
+ @jobs.dup
152
+ end
153
+ end
154
+
155
+ end
156
+ end
157
+
@@ -0,0 +1,339 @@
1
+ #--
2
+ # Copyright (c) 2006-2009, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+
26
+ module Rufus
27
+ module Scheduler
28
+
29
+ #
30
+ # The base class for all types of jobs.
31
+ #
32
+ class Job
33
+
34
+ # A reference to the scheduler owning this job
35
+ #
36
+ attr_reader :scheduler
37
+
38
+ # The initial, raw, scheduling info (at / in / every / cron)
39
+ #
40
+ attr_reader :t
41
+
42
+ # When the job is actually running, this attribute will hold the
43
+ # thread in which the job is running.
44
+ # Can be used to determine if a job is actually running.
45
+ #
46
+ attr_reader :job_thread
47
+
48
+ # The job parameters (passed via the schedule method)
49
+ #
50
+ attr_reader :params
51
+
52
+ # The block to call when triggering
53
+ #
54
+ attr_reader :block
55
+
56
+ # Last time the job executed
57
+ # (for an {At|In}Job, it will mean 'not executed' if nil or when
58
+ # it got executed if set)
59
+ #
60
+ # (
61
+ # Last time job got triggered (most useful with EveryJob, but can be
62
+ # useful with remaining instances of At/InJob (are they done ?))
63
+ # )
64
+ #
65
+ attr_reader :last
66
+
67
+ # The identifier for this job.
68
+ #
69
+ attr_reader :job_id
70
+
71
+
72
+ # Instantiating the job.
73
+ #
74
+ def initialize (scheduler, t, params, &block)
75
+
76
+ @scheduler = scheduler
77
+ @t = t
78
+ @params = params
79
+ @block = block || params[:schedulable]
80
+
81
+ raise ArgumentError.new(
82
+ 'no block or :schedulable passed, nothing to schedule'
83
+ ) unless @block
84
+
85
+ @params[:tags] = Array(@params[:tags])
86
+
87
+ @job_id = params[:job_id] || "#{self.class.name}_#{self.object_id.to_s}"
88
+
89
+ determine_at
90
+ end
91
+
92
+ # Returns the list of tags attached to the job.
93
+ #
94
+ def tags
95
+
96
+ @params[:tags]
97
+ end
98
+
99
+ # Sets the list of tags attached to the job (Usually they are set
100
+ # via the schedule every/at/in/cron method).
101
+ #
102
+ def tags= (tags)
103
+
104
+ @params[:tags] = Array(tags)
105
+ end
106
+
107
+ # Generally returns the string/float/integer used to schedule the job
108
+ # (seconds, time string, date string)
109
+ #
110
+ def schedule_info
111
+
112
+ @t
113
+ end
114
+
115
+ # Triggers the job.
116
+ #
117
+ def trigger (t=Time.now)
118
+
119
+ @last = t
120
+
121
+ @scheduler.send(:trigger_job, @params[:blocking]) do
122
+ #
123
+ # Note that #trigger_job is protected, hence the #send
124
+ # (Only jobs know about this method of the scheduler)
125
+
126
+ @job_thread = Thread.current
127
+
128
+ begin
129
+
130
+ #args = prepare_args
131
+ #@block.call(*args)
132
+
133
+ #@block.call(self)
134
+
135
+ @block.respond_to?(:call) ?
136
+ @block.call(self) : @block.trigger(@params)
137
+
138
+ @job_thread = nil
139
+
140
+ rescue Exception => e
141
+
142
+ @scheduler.handle_exception(self, e)
143
+ end
144
+ end
145
+
146
+ # note that add_job and add_cron_job ensured that :blocking is
147
+ # not used along :timeout
148
+
149
+ if to = @params[:timeout]
150
+
151
+ @scheduler.in(to, :tags => 'timeout') do
152
+
153
+ # at this point, @job_thread might be set
154
+
155
+ @job_thread.raise(Rufus::Scheduler::TimeOutError) \
156
+ if @job_thread and @job_thread.alive?
157
+ end
158
+ end
159
+ end
160
+
161
+ # Unschedules this job.
162
+ #
163
+ def unschedule
164
+
165
+ @scheduler.unschedule(self.job_id)
166
+ end
167
+
168
+ #--
169
+ #protected
170
+ #
171
+ # Prepare the args given the triggered block arity.
172
+ #
173
+ #def prepare_args
174
+ # if @scheduler.options[:onezero_block_arity]
175
+ # case @block.arity
176
+ # when 0 then []
177
+ # when 1 then [ @params ]
178
+ # when 2 then [ @job_id, @params ]
179
+ # #else [ @job_id, schedule_info, @params ]
180
+ # else [ @job_id, self, @params ]
181
+ # end
182
+ # else
183
+ # [ self ]
184
+ # end
185
+ #end
186
+ #++
187
+ end
188
+
189
+ #
190
+ # The base class of at/in/every jobs.
191
+ #
192
+ class SimpleJob < Job
193
+
194
+ # When the job is supposed to trigger
195
+ #
196
+ attr_reader :at
197
+
198
+ attr_reader :last
199
+
200
+ def determine_at
201
+
202
+ @at = Rufus.at_to_f(@t)
203
+ end
204
+ end
205
+
206
+ #
207
+ # Job that occurs once, in a certain amount of time.
208
+ #
209
+ class InJob < SimpleJob
210
+
211
+ protected
212
+
213
+ def determine_at
214
+
215
+ iin = @t.is_a?(Fixnum) || @t.is_a?(Float) ?
216
+ @t : Rufus.parse_duration_string(@t)
217
+
218
+ @at = (Time.now + iin).to_f
219
+ end
220
+ end
221
+
222
+ #
223
+ # Job that occurs once, at a certain point in time.
224
+ #
225
+ class AtJob < SimpleJob
226
+ end
227
+
228
+ #
229
+ # Recurring job with a certain frequency.
230
+ #
231
+ class EveryJob < SimpleJob
232
+
233
+ # The frequency, in seconds, of this EveryJob
234
+ #
235
+ attr_reader :frequency
236
+
237
+ def initialize (scheduler, t, params, &block)
238
+ super
239
+ determine_frequency
240
+ determine_at
241
+ end
242
+
243
+ # Triggers the job (and reschedules it).
244
+ #
245
+ def trigger
246
+
247
+ schedule_next
248
+
249
+ super
250
+
251
+ #unschedule if @params[:dont_reschedule]
252
+ # obsolete
253
+ end
254
+
255
+ protected
256
+
257
+ def determine_frequency
258
+
259
+ @frequency = @t.is_a?(Fixnum) || @t.is_a?(Float) ?
260
+ @t : Rufus.parse_duration_string(@t)
261
+ end
262
+
263
+ def determine_at
264
+
265
+ return unless @frequency
266
+
267
+ @last = @at
268
+ # the first time, @last will be nil
269
+
270
+ @at = if @last
271
+ @last + @frequency
272
+ else
273
+ if fi = @params[:first_in]
274
+ Time.now.to_f + Rufus.duration_to_f(fi)
275
+ elsif fa = @params[:first_at]
276
+ Rufus.at_to_f(fa)
277
+ else
278
+ Time.now.to_f + @frequency
279
+ end
280
+ end
281
+ end
282
+
283
+ # It's an every job, have to schedule next time it occurs...
284
+ #
285
+ def schedule_next
286
+
287
+ determine_at
288
+
289
+ @scheduler.send(:add_job, self)
290
+ end
291
+ end
292
+
293
+ #
294
+ # Recurring job, cron style.
295
+ #
296
+ class CronJob < Job
297
+
298
+ # The CronLine instance, it holds all the info about the cron schedule
299
+ #
300
+ attr_reader :cron_line
301
+
302
+ # The job parameters (passed via the schedule method)
303
+ #
304
+ attr_reader :params
305
+
306
+ # The block to call when triggering
307
+ #
308
+ attr_reader :block
309
+
310
+ # Creates a new CronJob instance.
311
+ #
312
+ def initialize (scheduler, cron_string, params, &block)
313
+
314
+ super
315
+
316
+ @cron_line = case @t
317
+
318
+ when String then CronLine.new(@t)
319
+ when CronLine then @t
320
+
321
+ else raise "cannot initialize a CronJob out of #{@t.inspect}"
322
+ end
323
+ end
324
+
325
+ def trigger_if_matches (time)
326
+
327
+ trigger(time) if @cron_line.matches?(time)
328
+ end
329
+
330
+ protected
331
+
332
+ def determine_at
333
+ # empty
334
+ end
335
+ end
336
+
337
+ end
338
+ end
339
+