rufus-scheduler 3.4.2 → 3.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/TODO.txt DELETED
@@ -1,151 +0,0 @@
1
-
2
- [o] merge schedule_queue and unschedule_queue (and merge [un]schedule steps)
3
- [x] OR stop using queue, since we've got the thread-safe JobArray
4
- [x] if possible, drop the mutex in JobArray
5
- NO, that mutex is necessary for Scheduler#jobs (on JRuby an co)...
6
- [o] named mutexes
7
- [o] drop the schedule queue, rely on the mutex in JobArray
8
- [o] def jobs; (@jobs.to_a + running_jobs).uniq; end
9
- [o] replace @unscheduled by @unscheduled_at
10
- [o] make sure #jobs doesn't return unscheduled jobs
11
- [o] job tags and find_by_tag(t) (as in rs 2.x)
12
- [o] require tzinfo anyway (runtime dep)
13
- [o] document frequency
14
- [o] accept :frequency => '5s'
15
- [o] timeout (as in rufus-scheduler 2.x)
16
- [o] Rufus::Scheduler#running_jobs (as in rufus-scheduler 2.x)
17
- [o] Rufus::Scheduler#terminate_all_jobs
18
- [o] Rufus::Scheduler::Job#kill
19
- [x] Rufus::Scheduler#kill_all_jobs
20
- [o] Rufus::Scheduler#shutdown(:terminate or :kill (or nothing))
21
- [o] RepeatJob #pause / #resume (think about discard past)
22
- [o] Rufus::Scheduler.start_new (backward comp) (with deprec note?)
23
- [o] pass job to scheduled block? What does rs 2.x do?
24
- [o] :first[_in|_at] for RepeatJob
25
- [o] :last[_in|_at] for RepeatJob
26
- [o] :times for RepeatJob (how many recurrences)
27
- [o] fix issue #39 (first_at parses as UTC)
28
- [o] about issue #43, raise if cron/every job frequency < scheduler frequency
29
- [o] unlock spec/parse_spec.rb:30 "parse datimes with timezones"
30
- [o] some kind of Schedulable (Xyz#call(job, time))
31
- [o] add Jruby and Rubinius to Travis
32
- [o] make Job #first_at= / #last_at= automatically parse strings?
33
- [o] bring in Kratob's spec about mutex vs timeout and adapt 3.0 to it,
34
- https://github.com/jmettraux/rufus-scheduler/pull/67
35
- [x] :unschedule_if => lambda { |job| ... }
36
- [o] OR look at how it was done in rs 2.0.x, some return value?
37
- no, pass the job as arg to the block, then let the block do job.unschedule
38
- so, document schedule.every('10d') { |j| j.unschedule if x?() }
39
- [x] remove the time in job.trigger(time)
40
- [o] add spec for job queued then unscheduled
41
- [o] add spec for Scheduler#shutdown and work threads
42
- [o] at some point, bring back rbx19 to Travis
43
- [o] move the parse/util part of scheduler.rb to util.rb
44
- [o] rescue KillSignal in job thread loop to kill just the job
45
- [o] add spec for raise if scheduling a job while scheduler is shutting down
46
- [o] schedule_in(2.days.from_now) {}
47
- at and in could understand each others time parameter, ftw...
48
- use the new #parse_to_time? no
49
- [o] do repeat jobs reschedule after timing out? yes
50
- [o] schedule_interval('20s')?
51
- [x] Scheduler#reschedule(job) (new copy of the job)
52
- [x] #free_all_work_threads is missing an implementation
53
- [x] rescue StandardError
54
- :on_error => :crash[_scheduler]
55
- :on_error => :ignore
56
- :on_error => ...
57
- [o] on_error: what about TimeoutError in that scheme?
58
- TimeoutError goes to $stderr, like a normal error
59
- [o] link to SO for support
60
- - sublink to "how to report bugs effectively"
61
- [o] link to #ruote for support
62
- [x] lockblock? pass a block to teach the scheduler how to lock?
63
- is not necessary, @scheduler = Scheduler.new if should_start?
64
- the surrounding Ruby code checks
65
- [o] introduce job "vars", as in
66
- http://stackoverflow.com/questions/18202848/how-to-have-a-variable-that-will-available-to-particular-scheduled-task-whenever
67
- or job['key'] Job #[] and #[]=, as with Thread #[] #[]=
68
- job-local variables #keys #key?
69
- [o] thread-safety for job-local variables?
70
- [x] discard past? discard_past => true or => "1d"
71
- default would be discard_past => "1m" or scheduler freq * 2 ?
72
- jobs would adjust their next_time until it fits the window...
73
- ~~ discard past by default
74
- [o] expanded block/schedulable (it's "callable")
75
- ```
76
- scheduler.every '10m' do
77
- def pre
78
- return false if Backend.down?
79
- # ...
80
- end
81
- def post
82
- # ...
83
- end
84
- def trigger
85
- puts "oh hai!"
86
- end
87
- end
88
- ```
89
- or something like that...
90
- ...
91
- OR accept a class (and instantiate it the first time)
92
- ```
93
- scheduler.every '10m', Class.new do
94
- def call(job, time)
95
- # ...
96
- end
97
- end
98
- ```
99
- the job contains the instance in its @callable
100
- [x] add spec case for corner case in Job#trigger (overlap vs reschedule) !!!
101
- [o] rethink job array vs job set for #scheduled?
102
- [x] introduce common parent class for EveryJob and IntervalJob
103
- [o] create spec/ at_job_spec.rb, repeat_job_spec.rb, cron_job_spec.rb, ...
104
- [x] ensure EveryJob do not schedule in the past (it's already like that)
105
- [o] CronLine#next_time should return a time with subseconds chopped off
106
- [o] drop min work threads setting?
107
- [o] thread pool something? Thread upper limit?
108
- [o] Rufus::Scheduler.singleton, Rufus::Scheduler.s
109
- [o] EveryJob#first_at= and IntervalJob#first_at= should alter @next_time
110
- [o] scheduler.schedule duration/time/cron ... for at/in/cron
111
- (not every, nor interval)
112
- scheduler.repeat time/cron ... for every/cron
113
-
114
- [o] :lockfile => x, timestamp, process_id, thread_id...
115
- warning: have to clean up that file on exit... or does the scheduler
116
- timestamps it?
117
- [ ] develop lockfile timestamp thinggy
118
- ~ if the timestamp is too old (twice the default frequency?) then
119
- lock [file] take over...
120
- Is that really what we want all the time?
121
-
122
- [ ] idea: :mutex => x and :skip_on_mutex => true ?
123
- would prevent blocking/waiting for the mutex to get available
124
- :mutex => [ "mutex_name", true ]
125
- :mutex => [ [ "mutex_name", true ], [ "other_mutex_name", false ] ]
126
-
127
- [ ] bring back EM (but only EM.defer ?) :defer => true (Job or Scheduler
128
- or both option?)
129
-
130
- [ ] prepare a daemon, trust daemon-kit for that
131
-
132
- [ ] :if => lambda { |job, time| ... } why not?
133
- :unless => lambda { ...
134
- :block => lambda { ...
135
- can help get the block themselves leaner
136
- #
137
- investigate guards for schedulables... def if_guard; ...; end
138
-
139
- [ ] scheduler.every '10', Class.new do
140
- def call(job, time)
141
- # might fail...
142
- end
143
- def on_error(err, job)
144
- # catches...
145
- end
146
- end
147
-
148
- ~~~
149
-
150
- [ ] scheduler.at('chronic string', chronic_options...)
151
-
data/fail.txt DELETED
@@ -1,2 +0,0 @@
1
- rspec ./spec/job_spec.rb:465 # Rufus::Scheduler::Job :mutex :mutex => [ array_of_mutex_names_or_instances ] prevents concurrent executions
2
- rspec ./spec/scheduler_spec.rb:534 # Rufus::Scheduler#running_jobs(:tag/:tags => x) returns a list of running jobs filtered by tag
data/fail18.txt DELETED
@@ -1,12 +0,0 @@
1
-
2
- rspec ./spec/job_spec.rb:640 # Rufus::Scheduler::Job work time #mean_work_time gathers work times and computes the mean
3
- rspec ./spec/schedule_at_spec.rb:60 # Rufus::Scheduler#at triggers a job
4
- rspec ./spec/schedule_in_spec.rb:44 # Rufus::Scheduler#in removes the job after execution
5
- rspec ./spec/scheduler_spec.rb:83 # Rufus::Scheduler a schedule method passes the job to its block when it triggers
6
- rspec ./spec/scheduler_spec.rb:534 # Rufus::Scheduler#running_jobs(:tag/:tags => x) returns a list of running jobs filtered by tag
7
- rspec ./spec/scheduler_spec.rb:601 # Rufus::Scheduler#occurrences(time0, time1) respects :times for repeat jobs
8
- rspec ./spec/scheduler_spec.rb:1019 # Rufus::Scheduler#on_post_trigger is called right after a job triggers
9
-
10
-
11
- determine_id specs are slower... much slower...
12
-
@@ -1,498 +0,0 @@
1
-
2
-
3
- class Rufus::Scheduler
4
-
5
- #
6
- # A 'cron line' is a line in the sense of a crontab
7
- # (man 5 crontab) file line.
8
- #
9
- class CronLine
10
-
11
- # The max number of years in the future or the past before giving up
12
- # searching for #next_time or #previous_time respectively
13
- #
14
- NEXT_TIME_MAX_YEARS = 14
15
-
16
- # The string used for creating this cronline instance.
17
- #
18
- attr_reader :original
19
- attr_reader :original_timezone
20
-
21
- attr_reader :seconds
22
- attr_reader :minutes
23
- attr_reader :hours
24
- attr_reader :days
25
- attr_reader :months
26
- #attr_reader :monthdays # reader defined below
27
- attr_reader :weekdays
28
- attr_reader :timezone
29
-
30
- def initialize(line)
31
-
32
- fail ArgumentError.new(
33
- "not a string: #{line.inspect}"
34
- ) unless line.is_a?(String)
35
-
36
- @original = line
37
- @original_timezone = nil
38
-
39
- items = line.split
40
-
41
- if @timezone = EoTime.get_tzone(items.last)
42
- @original_timezone = items.pop
43
- else
44
- @timezone = EoTime.local_tzone
45
- end
46
-
47
- fail ArgumentError.new(
48
- "not a valid cronline : '#{line}'"
49
- ) unless items.length == 5 or items.length == 6
50
-
51
- offset = items.length - 5
52
-
53
- @seconds = offset == 1 ? parse_item(items[0], 0, 59) : [ 0 ]
54
- @minutes = parse_item(items[0 + offset], 0, 59)
55
- @hours = parse_item(items[1 + offset], 0, 24)
56
- @days = parse_item(items[2 + offset], -30, 31)
57
- @months = parse_item(items[3 + offset], 1, 12)
58
- @weekdays, @monthdays = parse_weekdays(items[4 + offset])
59
-
60
- [ @seconds, @minutes, @hours, @months ].each do |es|
61
-
62
- fail ArgumentError.new(
63
- "invalid cronline: '#{line}'"
64
- ) if es && es.find { |e| ! e.is_a?(Integer) }
65
- end
66
-
67
- if @days && @days.include?(0) # gh-221
68
-
69
- fail ArgumentError.new('invalid day 0 in cronline')
70
- end
71
- end
72
-
73
- # Returns true if the given time matches this cron line.
74
- #
75
- def matches?(time)
76
-
77
- # FIXME Don't create a new EoTime if time is already a EoTime in same
78
- # zone ...
79
- # Wait, this seems only used in specs...
80
- t = EoTime.new(time.to_f, @timezone)
81
-
82
- return false unless sub_match?(t, :sec, @seconds)
83
- return false unless sub_match?(t, :min, @minutes)
84
- return false unless sub_match?(t, :hour, @hours)
85
- return false unless date_match?(t)
86
- true
87
- end
88
-
89
- # Returns the next time that this cron line is supposed to 'fire'
90
- #
91
- # This is raw, 3 secs to iterate over 1 year on my macbook :( brutal.
92
- # (Well, I was wrong, takes 0.001 sec on 1.8.7 and 1.9.1)
93
- #
94
- # This method accepts an optional Time parameter. It's the starting point
95
- # for the 'search'. By default, it's Time.now
96
- #
97
- # Note that the time instance returned will be in the same time zone that
98
- # the given start point Time (thus a result in the local time zone will
99
- # be passed if no start time is specified (search start time set to
100
- # Time.now))
101
- #
102
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
103
- # Time.mktime(2008, 10, 24, 7, 29))
104
- # #=> Fri Oct 24 07:30:00 -0500 2008
105
- #
106
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
107
- # Time.utc(2008, 10, 24, 7, 29))
108
- # #=> Fri Oct 24 07:30:00 UTC 2008
109
- #
110
- # Rufus::Scheduler::CronLine.new('30 7 * * *').next_time(
111
- # Time.utc(2008, 10, 24, 7, 29)).localtime
112
- # #=> Fri Oct 24 02:30:00 -0500 2008
113
- #
114
- # (Thanks to K Liu for the note and the examples)
115
- #
116
- def next_time(from=EoTime.now)
117
-
118
- nt = nil
119
- zt = EoTime.new(from.to_i + 1, @timezone)
120
- maxy = from.year + NEXT_TIME_MAX_YEARS
121
-
122
- loop do
123
-
124
- nt = zt.dup
125
-
126
- fail RangeError.new(
127
- "failed to reach occurrence within " +
128
- "#{NEXT_TIME_MAX_YEARS} years for '#{original}'"
129
- ) if nt.year > maxy
130
-
131
- unless date_match?(nt)
132
- zt.add((24 - nt.hour) * 3600 - nt.min * 60 - nt.sec)
133
- next
134
- end
135
- unless sub_match?(nt, :hour, @hours)
136
- zt.add((60 - nt.min) * 60 - nt.sec)
137
- next
138
- end
139
- unless sub_match?(nt, :min, @minutes)
140
- zt.add(60 - nt.sec)
141
- next
142
- end
143
- unless sub_match?(nt, :sec, @seconds)
144
- zt.add(next_second(nt))
145
- next
146
- end
147
-
148
- break
149
- end
150
-
151
- nt
152
- end
153
-
154
- # Returns the previous time the cronline matched. It's like next_time, but
155
- # for the past.
156
- #
157
- def previous_time(from=EoTime.now)
158
-
159
- pt = nil
160
- zt = EoTime.new(from.to_i - 1, @timezone)
161
- miny = from.year - NEXT_TIME_MAX_YEARS
162
-
163
- loop do
164
-
165
- pt = zt.dup
166
-
167
- fail RangeError.new(
168
- "failed to reach occurrence within " +
169
- "#{NEXT_TIME_MAX_YEARS} years for '#{original}'"
170
- ) if pt.year < miny
171
-
172
- unless date_match?(pt)
173
- zt.subtract(pt.hour * 3600 + pt.min * 60 + pt.sec + 1)
174
- next
175
- end
176
- unless sub_match?(pt, :hour, @hours)
177
- zt.subtract(pt.min * 60 + pt.sec + 1)
178
- next
179
- end
180
- unless sub_match?(pt, :min, @minutes)
181
- zt.subtract(pt.sec + 1)
182
- next
183
- end
184
- unless sub_match?(pt, :sec, @seconds)
185
- zt.subtract(prev_second(pt))
186
- next
187
- end
188
-
189
- break
190
- end
191
-
192
- pt
193
- end
194
-
195
- # Returns an array of 6 arrays (seconds, minutes, hours, days,
196
- # months, weekdays).
197
- # This method is mostly used by the cronline specs.
198
- #
199
- def to_a
200
-
201
- [
202
- toa(@seconds),
203
- toa(@minutes),
204
- toa(@hours),
205
- toa(@days),
206
- toa(@months),
207
- toa(@weekdays),
208
- toa(@monthdays),
209
- @timezone.name
210
- ]
211
- end
212
- alias to_array to_a
213
-
214
- # Returns a quickly computed approximation of the frequency for this
215
- # cron line.
216
- #
217
- # #brute_frequency, on the other hand, will compute the frequency by
218
- # examining a whole year, that can take more than seconds for a seconds
219
- # level cron...
220
- #
221
- def frequency
222
-
223
- return brute_frequency unless @seconds && @seconds.length > 1
224
-
225
- secs = toa(@seconds)
226
-
227
- secs[1..-1].inject([ secs[0], 60 ]) { |(prev, delta), sec|
228
- d = sec - prev
229
- [ sec, d < delta ? d : delta ]
230
- }[1]
231
- end
232
-
233
- # Caching facility. Currently only used for brute frequencies.
234
- #
235
- @cache = {}; class << self; attr_reader :cache; end
236
-
237
- # Returns the shortest delta between two potential occurrences of the
238
- # schedule described by this cronline.
239
- #
240
- # .
241
- #
242
- # For a simple cronline like "*/5 * * * *", obviously the frequency is
243
- # five minutes. Why does this method look at a whole year of #next_time ?
244
- #
245
- # Consider "* * * * sun#2,sun#3", the computed frequency is 1 week
246
- # (the shortest delta is the one between the second sunday and the third
247
- # sunday). This method takes no chance and runs next_time for the span
248
- # of a whole year and keeps the shortest.
249
- #
250
- # Of course, this method can get VERY slow if you call on it a second-
251
- # based cronline...
252
- #
253
- def brute_frequency
254
-
255
- key = "brute_frequency:#{@original}"
256
-
257
- delta = self.class.cache[key]
258
- return delta if delta
259
-
260
- delta = 366 * DAY_S
261
-
262
- t0 = previous_time(Time.local(2000, 1, 1))
263
-
264
- loop do
265
-
266
- break if delta <= 1
267
- break if delta <= 60 && @seconds && @seconds.size == 1
268
-
269
- #st = Time.now
270
- t1 = next_time(t0)
271
- #p Time.now - st
272
- d = t1 - t0
273
- delta = d if d < delta
274
- break if @months.nil? && t1.month == 2
275
- break if @months.nil? && @days.nil? && t1.day == 2
276
- break if @months.nil? && @days.nil? && @hours.nil? && t1.hour == 1
277
- break if @months.nil? && @days.nil? && @hours.nil? && @minutes.nil? && t1.min == 1
278
- break if t1.year >= 2001
279
-
280
- t0 = t1
281
- end
282
-
283
- self.class.cache[key] = delta
284
- end
285
-
286
- def next_second(time)
287
-
288
- secs = toa(@seconds)
289
-
290
- return secs.first + 60 - time.sec if time.sec > secs.last
291
-
292
- secs.shift while secs.first < time.sec
293
-
294
- secs.first - time.sec
295
- end
296
-
297
- def prev_second(time)
298
-
299
- secs = toa(@seconds)
300
-
301
- return time.sec + 60 - secs.last if time.sec < secs.first
302
-
303
- secs.pop while time.sec < secs.last
304
-
305
- time.sec - secs.last
306
- end
307
-
308
- protected
309
-
310
- def sc_sort(a)
311
-
312
- a.sort_by { |e| e.is_a?(String) ? 61 : e.to_i }
313
- end
314
-
315
- if RUBY_VERSION >= '1.9'
316
- def toa(item)
317
- item == nil ? nil : item.to_a
318
- end
319
- else
320
- def toa(item)
321
- item.is_a?(Set) ? sc_sort(item.to_a) : item
322
- end
323
- end
324
-
325
- WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
326
- DAY_S = 24 * 3600
327
-
328
- def parse_weekdays(item)
329
-
330
- return nil if item == '*'
331
-
332
- weekdays = nil
333
- monthdays = nil
334
-
335
- item.downcase.split(',').each do |it|
336
-
337
- WEEKDAYS.each_with_index { |a, i| it.gsub!(/#{a}/, i.to_s) }
338
-
339
- it = it.gsub(/([^#])l/, '\1#-1')
340
- # "5L" == "5#-1" == the last Friday
341
-
342
- if m = it.match(/\A(.+)#(l|-?[12345])\z/)
343
-
344
- fail ArgumentError.new(
345
- "ranges are not supported for monthdays (#{it})"
346
- ) if m[1].index('-')
347
-
348
- it = it.gsub(/#l/, '#-1')
349
-
350
- (monthdays ||= []) << it
351
-
352
- else
353
-
354
- fail ArgumentError.new(
355
- "invalid weekday expression (#{item})"
356
- ) if it !~ /\A0*[0-7](-0*[0-7])?\z/
357
-
358
- its = it.index('-') ? parse_range(it, 0, 7) : [ Integer(it) ]
359
- its = its.collect { |i| i == 7 ? 0 : i }
360
-
361
- (weekdays ||= []).concat(its)
362
- end
363
- end
364
-
365
- weekdays = weekdays.uniq.sort if weekdays
366
-
367
- [ weekdays, monthdays ]
368
- end
369
-
370
- def parse_item(item, min, max)
371
-
372
- return nil if item == '*'
373
-
374
- r = item.split(',').map { |i| parse_range(i.strip, min, max) }.flatten
375
-
376
- fail ArgumentError.new(
377
- "found duplicates in #{item.inspect}"
378
- ) if r.uniq.size < r.size
379
-
380
- r = sc_sort(r)
381
-
382
- Set.new(r)
383
- end
384
-
385
- RANGE_REGEX = /\A(\*|-?\d{1,2})(?:-(-?\d{1,2}))?(?:\/(\d{1,2}))?\z/
386
-
387
- def parse_range(item, min, max)
388
-
389
- return %w[ L ] if item == 'L'
390
-
391
- item = '*' + item if item[0, 1] == '/'
392
-
393
- m = item.match(RANGE_REGEX)
394
-
395
- fail ArgumentError.new(
396
- "cannot parse #{item.inspect}"
397
- ) unless m
398
-
399
- mmin = min == -30 ? 1 : min # days
400
-
401
- sta = m[1]
402
- sta = sta == '*' ? mmin : sta.to_i
403
-
404
- edn = m[2]
405
- edn = edn ? edn.to_i : sta
406
- edn = max if m[1] == '*'
407
-
408
- inc = m[3]
409
- inc = inc ? inc.to_i : 1
410
-
411
- fail ArgumentError.new(
412
- "#{item.inspect} positive/negative ranges not allowed"
413
- ) if (sta < 0 && edn > 0) || (sta > 0 && edn < 0)
414
-
415
- fail ArgumentError.new(
416
- "#{item.inspect} descending day ranges not allowed"
417
- ) if min == -30 && sta > edn
418
-
419
- fail ArgumentError.new(
420
- "#{item.inspect} is not in range #{min}..#{max}"
421
- ) if sta < min || edn > max
422
-
423
- fail ArgumentError.new(
424
- "#{item.inspect} increment must be greater than zero"
425
- ) if inc == 0
426
-
427
- r = []
428
- val = sta
429
-
430
- loop do
431
- v = val
432
- v = 0 if max == 24 && v == 24 # hours
433
- r << v
434
- break if inc == 1 && val == edn
435
- val += inc
436
- break if inc > 1 && val > edn
437
- val = min if val > max
438
- end
439
-
440
- r.uniq
441
- end
442
-
443
- # FIXME: Eventually split into day_match?, hour_match? and monthdays_match?o
444
- #
445
- def sub_match?(time, accessor, values)
446
-
447
- return true if values.nil?
448
-
449
- value = time.send(accessor)
450
-
451
- if accessor == :day
452
-
453
- values.each do |v|
454
- return true if v == 'L' && (time + DAY_S).day == 1
455
- return true if v.to_i < 0 && (time + (1 - v) * DAY_S).day == 1
456
- end
457
- end
458
-
459
- if accessor == :hour
460
-
461
- return true if value == 0 && values.include?(24)
462
- end
463
-
464
- if accessor == :monthdays
465
-
466
- return true if (values & value).any?
467
- end
468
-
469
- values.include?(value)
470
- end
471
-
472
- # def monthday_match?(zt, values)
473
- #
474
- # return true if values.nil?
475
- #
476
- # today_values = monthdays(zt)
477
- #
478
- # (today_values & values).any?
479
- # end
480
-
481
- def date_match?(zt)
482
-
483
- return false unless sub_match?(zt, :day, @days)
484
- return false unless sub_match?(zt, :month, @months)
485
-
486
- return true if (
487
- (@weekdays && @monthdays) &&
488
- (sub_match?(zt, :wday, @weekdays) ||
489
- sub_match?(zt, :monthdays, @monthdays)))
490
-
491
- return false unless sub_match?(zt, :wday, @weekdays)
492
- return false unless sub_match?(zt, :monthdays, @monthdays)
493
-
494
- true
495
- end
496
- end
497
- end
498
-