rufus-scheduler 3.0.2 → 3.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +7 -0
- data/CREDITS.txt +2 -0
- data/README.md +105 -0
- data/lib/rufus/scheduler.rb +34 -4
- data/lib/rufus/scheduler/cronline.rb +63 -23
- data/lib/rufus/scheduler/jobs.rb +9 -8
- data/spec/cronline_spec.rb +1 -0
- data/spec/custom_locks_spec.rb +47 -0
- data/spec/scheduler_spec.rb +15 -0
- metadata +4 -3
data/CHANGELOG.txt
CHANGED
@@ -2,6 +2,13 @@
|
|
2
2
|
= rufus-scheduler CHANGELOG.txt
|
3
3
|
|
4
4
|
|
5
|
+
== rufus-scheduler - 3.0.3 released 2013/12/12
|
6
|
+
|
7
|
+
- CronLine#previous_time fix by Yassen Bantchev (https://github.com/yassenb)
|
8
|
+
- introduce ZookeptScheduler example in the readme
|
9
|
+
- rename #consider_lockfile to #lock and introduce #unlock
|
10
|
+
|
11
|
+
|
5
12
|
== rufus-scheduler - 3.0.2 released 2013/10/22
|
6
13
|
|
7
14
|
- default :max_work_threads to 28
|
data/CREDITS.txt
CHANGED
@@ -4,6 +4,8 @@
|
|
4
4
|
|
5
5
|
== Contributors
|
6
6
|
|
7
|
+
- Yassen Bantchev (https://github.com/yassenb) CronLine#previous_time rewrite
|
8
|
+
- Eric Lindvall (https://github.com/eric) Zookeeper locked example
|
7
9
|
- Ted Pennings (https://github.com/tedpennings) typo in post_install_message
|
8
10
|
- Tobias Kraze (https://github.com/kratob) timeout vs mutex fix
|
9
11
|
- Patrick Farrell (https://github.com/pfarrell) pointing at deprecated start_new
|
data/README.md
CHANGED
@@ -802,6 +802,10 @@ Shuts down the scheduler, waits (blocks) until all the jobs cease running.
|
|
802
802
|
|
803
803
|
Kills all the job (threads) and then shuts the scheduler down. Radical.
|
804
804
|
|
805
|
+
### Scheduler#down?
|
806
|
+
|
807
|
+
Returns true if the scheduler has been shut down.
|
808
|
+
|
805
809
|
### Scheduler#join
|
806
810
|
|
807
811
|
Let's the current thread join the scheduling thread in rufus-scheduler. The thread comes back when the scheduler gets shut down.
|
@@ -964,6 +968,8 @@ The idea is to guarantee only one scheduler (in a group of scheduler sharing the
|
|
964
968
|
|
965
969
|
This is useful in environments where the Ruby process holding the scheduler gets started multiple times.
|
966
970
|
|
971
|
+
If the lockfile mechanism here is not sufficient, you can plug your custom mechanism. It's explained in [advanced lock schemes](#advanced-lock-schemes) below.
|
972
|
+
|
967
973
|
### :max_work_threads
|
968
974
|
|
969
975
|
In rufus-scheduler 2.x, by default, each job triggering received its own, new, hthread of execution. In rufus-scheduler 3.x, execution happens in a work thread and the max work thread count defaults to 28.
|
@@ -1004,6 +1010,53 @@ Rufus::Scheduler.s.every '10s' { puts "hello, world!" }
|
|
1004
1010
|
```
|
1005
1011
|
|
1006
1012
|
|
1013
|
+
## advanced lock schemes
|
1014
|
+
|
1015
|
+
As seen above, rufus-scheduler proposes the :lockfile system out of the box. If in a group of schedulers only one is supposed to run, the lockfile mecha prevents schedulers that have not set/created the lockfile from running.
|
1016
|
+
|
1017
|
+
There are situation where this is not sufficient.
|
1018
|
+
|
1019
|
+
By overriding #lock and #unlock, one can customize how his schedulers lock.
|
1020
|
+
|
1021
|
+
This example was provided by [Eric Lindvall](https://github.com/eric):
|
1022
|
+
|
1023
|
+
```ruby
|
1024
|
+
class ZookeptScheduler < Rufus::Scheduler
|
1025
|
+
|
1026
|
+
def initialize(zookeeper, opts={})
|
1027
|
+
@zk = zookeeper
|
1028
|
+
super(opts)
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
def lock
|
1032
|
+
@zk_locker = @zk.exclusive_locker('scheduler')
|
1033
|
+
@zk_locker.lock # returns true if the lock was acquired, false else
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
def unlock
|
1037
|
+
@zk_locker.unlock
|
1038
|
+
end
|
1039
|
+
|
1040
|
+
def confirm_lock
|
1041
|
+
return false if down?
|
1042
|
+
@zk_locker.assert!
|
1043
|
+
rescue ZK::Exceptions::LockAssertionFailedError => e
|
1044
|
+
# we've lost the lock, shutdown (and return false to at least prevent
|
1045
|
+
# this job from triggering
|
1046
|
+
shutdown
|
1047
|
+
false
|
1048
|
+
end
|
1049
|
+
end
|
1050
|
+
```
|
1051
|
+
|
1052
|
+
This uses a [zookeeper](http://zookeeper.apache.org/) to make sure only one scheduler in a group of distributed schedulers runs.
|
1053
|
+
|
1054
|
+
The methods #lock and #unlock are overriden and #confirm_lock is provided,
|
1055
|
+
to make sure that the lock is still valid.
|
1056
|
+
|
1057
|
+
The #confirm_lock method is called right before a job triggers (if it is provided). The more generic callback #on_pre_trigger is called right after #confirm_lock.
|
1058
|
+
|
1059
|
+
|
1007
1060
|
## parsing cronlines and time strings
|
1008
1061
|
|
1009
1062
|
Rufus::Scheduler provides a class method ```.parse``` to parse time durations and cron strings. It's what it's using when receiving schedules. One can use it diectly (no need to instantiate a Scheduler).
|
@@ -1052,6 +1105,58 @@ Rufus::Scheduler.to_duration_hash(62.127, :drop_seconds => true)
|
|
1052
1105
|
# => { :m => 1 }
|
1053
1106
|
```
|
1054
1107
|
|
1108
|
+
### cronline notations specific to rufus-scheduler
|
1109
|
+
|
1110
|
+
#### first Monday, last Sunday et al
|
1111
|
+
|
1112
|
+
To schedule something at noon every first Monday of the month:
|
1113
|
+
|
1114
|
+
```ruby
|
1115
|
+
scheduler.cron('00 12 * * mon#1') do
|
1116
|
+
# ...
|
1117
|
+
end
|
1118
|
+
```
|
1119
|
+
|
1120
|
+
To schedule something at noon the last Sunday of every month:
|
1121
|
+
|
1122
|
+
```ruby
|
1123
|
+
scheduler.cron('00 12 * * sun#-1') do
|
1124
|
+
# ...
|
1125
|
+
end
|
1126
|
+
#
|
1127
|
+
# OR
|
1128
|
+
#
|
1129
|
+
scheduler.cron('00 12 * * sun#L') do
|
1130
|
+
# ...
|
1131
|
+
end
|
1132
|
+
```
|
1133
|
+
|
1134
|
+
Such cronlines can be tested with scripts like:
|
1135
|
+
|
1136
|
+
```ruby
|
1137
|
+
require 'rufus-scheduler'
|
1138
|
+
|
1139
|
+
Time.now
|
1140
|
+
# => 2013-10-26 07:07:08 +0900
|
1141
|
+
Rufus::Scheduler.parse('* * * * mon#1').next_time
|
1142
|
+
# => 2013-11-04 00:00:00 +0900
|
1143
|
+
```
|
1144
|
+
|
1145
|
+
#### L (last day of month)
|
1146
|
+
|
1147
|
+
L can be used in the "day" slot:
|
1148
|
+
|
1149
|
+
In this example, the cronline is supposed to trigger every last day of the month at noon:
|
1150
|
+
|
1151
|
+
```ruby
|
1152
|
+
require 'rufus-scheduler'
|
1153
|
+
Time.now
|
1154
|
+
# => 2013-10-26 07:22:09 +0900
|
1155
|
+
Rufus::Scheduler.parse('00 12 L * *').next_time
|
1156
|
+
# => 2013-10-31 12:00:00 +0900
|
1157
|
+
```
|
1158
|
+
|
1159
|
+
|
1055
1160
|
## a note about timezones
|
1056
1161
|
|
1057
1162
|
Cron schedules and at schedules support the specification of a timezone.
|
data/lib/rufus/scheduler.rb
CHANGED
@@ -38,7 +38,7 @@ module Rufus
|
|
38
38
|
require 'rufus/scheduler/cronline'
|
39
39
|
require 'rufus/scheduler/job_array'
|
40
40
|
|
41
|
-
VERSION = '3.0.
|
41
|
+
VERSION = '3.0.3'
|
42
42
|
|
43
43
|
#
|
44
44
|
# A common error class for rufus-scheduler
|
@@ -93,7 +93,7 @@ module Rufus
|
|
93
93
|
|
94
94
|
@thread_key = "rufus_scheduler_#{self.object_id}"
|
95
95
|
|
96
|
-
|
96
|
+
lock || return
|
97
97
|
|
98
98
|
start
|
99
99
|
end
|
@@ -134,7 +134,7 @@ module Rufus
|
|
134
134
|
kill_all_work_threads
|
135
135
|
end
|
136
136
|
|
137
|
-
|
137
|
+
unlock
|
138
138
|
end
|
139
139
|
|
140
140
|
alias stop shutdown
|
@@ -158,6 +158,11 @@ module Rufus
|
|
158
158
|
@thread.join
|
159
159
|
end
|
160
160
|
|
161
|
+
def down?
|
162
|
+
|
163
|
+
! @started_at
|
164
|
+
end
|
165
|
+
|
161
166
|
def paused?
|
162
167
|
|
163
168
|
@paused
|
@@ -403,7 +408,25 @@ module Rufus
|
|
403
408
|
end
|
404
409
|
end
|
405
410
|
|
406
|
-
|
411
|
+
# Returns true if the scheduler has acquired the [exclusive] lock and
|
412
|
+
# thus may run.
|
413
|
+
#
|
414
|
+
# Most of the time, a scheduler is run alone and this method should
|
415
|
+
# return true. It is useful in cases where among a group of applications
|
416
|
+
# only one of them should run the scheduler. For schedulers that should
|
417
|
+
# not run, the method should return false.
|
418
|
+
#
|
419
|
+
# Out of the box, rufus-scheduler proposes the
|
420
|
+
# :lockfile => 'path/to/lock/file' scheduler start option. It makes
|
421
|
+
# it easy for schedulers on the same machine to determine which should
|
422
|
+
# run (to first to write the lockfile and lock it). It uses "man 2 flock"
|
423
|
+
# so it probably won't work reliably on distributed file systems.
|
424
|
+
#
|
425
|
+
# If one needs to use a special/different locking mechanism, providing
|
426
|
+
# overriding implementation for this #lock and the #unlock complement is
|
427
|
+
# easy.
|
428
|
+
#
|
429
|
+
def lock
|
407
430
|
|
408
431
|
@lockfile = nil
|
409
432
|
|
@@ -433,6 +456,13 @@ module Rufus
|
|
433
456
|
true
|
434
457
|
end
|
435
458
|
|
459
|
+
# Sister method to #lock, is called when the scheduler shuts down.
|
460
|
+
#
|
461
|
+
def unlock
|
462
|
+
|
463
|
+
@lockfile.flock(File::LOCK_UN) if @lockfile
|
464
|
+
end
|
465
|
+
|
436
466
|
def terminate_all_jobs
|
437
467
|
|
438
468
|
jobs.each { |j| j.unschedule }
|
@@ -122,16 +122,13 @@ class Rufus::Scheduler
|
|
122
122
|
#
|
123
123
|
def next_time(from=Time.now)
|
124
124
|
|
125
|
-
time =
|
126
|
-
|
127
|
-
time = time.respond_to?(:round) ? time.round : time - time.usec * 1e-6
|
128
|
-
# chop off subseconds (and yes, Ruby 1.8 doesn't have #round)
|
125
|
+
time = local_time(from)
|
126
|
+
time = round_to_seconds(time)
|
129
127
|
|
128
|
+
# start at the next second
|
130
129
|
time = time + 1
|
131
|
-
# start at the next second
|
132
130
|
|
133
131
|
loop do
|
134
|
-
|
135
132
|
unless date_match?(time)
|
136
133
|
time += (24 - time.hour) * 3600 - time.min * 60 - time.sec; next
|
137
134
|
end
|
@@ -148,34 +145,38 @@ class Rufus::Scheduler
|
|
148
145
|
break
|
149
146
|
end
|
150
147
|
|
151
|
-
|
152
|
-
time = @timezone.local_to_utc(time)
|
153
|
-
time = time.getlocal unless from.utc?
|
154
|
-
end
|
155
|
-
|
156
|
-
time
|
148
|
+
global_time(time, from.utc?)
|
157
149
|
end
|
158
150
|
|
159
|
-
# Returns the previous the cronline matched. It's like next_time, but
|
151
|
+
# Returns the previous time the cronline matched. It's like next_time, but
|
160
152
|
# for the past.
|
161
153
|
#
|
162
154
|
def previous_time(from=Time.now)
|
163
155
|
|
164
|
-
|
165
|
-
|
166
|
-
# finds for '* * * * sun', '* * 13 * *' and '0 12 13 * *'
|
167
|
-
# starting 1970, 1, 1 in 1.8 to 2 seconds (says Rspec)
|
156
|
+
time = local_time(from)
|
157
|
+
time = round_to_seconds(time)
|
168
158
|
|
169
|
-
start
|
170
|
-
|
159
|
+
# start at the previous second
|
160
|
+
time = time - 1
|
171
161
|
|
172
162
|
loop do
|
173
|
-
|
174
|
-
|
175
|
-
|
163
|
+
unless date_match?(time)
|
164
|
+
time -= time.hour * 3600 + time.min * 60 + time.sec + 1; next
|
165
|
+
end
|
166
|
+
unless sub_match?(time, :hour, @hours)
|
167
|
+
time -= time.min * 60 + time.sec + 1; next
|
168
|
+
end
|
169
|
+
unless sub_match?(time, :min, @minutes)
|
170
|
+
time -= time.sec + 1; next
|
171
|
+
end
|
172
|
+
unless sub_match?(time, :sec, @seconds)
|
173
|
+
time -= 1; next
|
174
|
+
end
|
175
|
+
|
176
|
+
break
|
176
177
|
end
|
177
178
|
|
178
|
-
|
179
|
+
global_time(time, from.utc?)
|
179
180
|
end
|
180
181
|
|
181
182
|
# Returns an array of 6 arrays (seconds, minutes, hours, days,
|
@@ -199,6 +200,27 @@ class Rufus::Scheduler
|
|
199
200
|
# Returns the shortest delta between two potential occurences of the
|
200
201
|
# schedule described by this cronline.
|
201
202
|
#
|
203
|
+
# .
|
204
|
+
#
|
205
|
+
# For a simple cronline like "*/5 * * * *", obviously the frequency is
|
206
|
+
# five minutes. Why does this method look at a whole year of #next_time ?
|
207
|
+
#
|
208
|
+
# Consider "* * * * sun#2,sun#3", the computed frequency is 1 week
|
209
|
+
# (the shortest delta is the one between the second sunday and the third
|
210
|
+
# sunday). This method takes no chance and runs next_time for the span
|
211
|
+
# of a whole year and keeps the shortest.
|
212
|
+
#
|
213
|
+
# Of course, this method can get VERY slow if you call on it a second-
|
214
|
+
# based cronline...
|
215
|
+
#
|
216
|
+
# Since it's a rarely used method, I haven't taken the time to make it
|
217
|
+
# smarter/faster.
|
218
|
+
#
|
219
|
+
# One obvious improvement would be to cache the result once computed...
|
220
|
+
#
|
221
|
+
# See https://github.com/jmettraux/rufus-scheduler/issues/89
|
222
|
+
# for a discussion about this method.
|
223
|
+
#
|
202
224
|
def frequency
|
203
225
|
|
204
226
|
delta = 366 * DAY_S
|
@@ -380,6 +402,24 @@ class Rufus::Scheduler
|
|
380
402
|
|
381
403
|
[ "#{WEEKDAYS[date.wday]}##{pos}", "#{WEEKDAYS[date.wday]}##{neg}" ]
|
382
404
|
end
|
405
|
+
|
406
|
+
def local_time(time)
|
407
|
+
time = @timezone ? @timezone.utc_to_local(time.getutc) : time
|
408
|
+
end
|
409
|
+
|
410
|
+
def global_time(time, from_in_utc)
|
411
|
+
if @timezone
|
412
|
+
time = @timezone.local_to_utc(time)
|
413
|
+
time = time.getlocal unless from_in_utc
|
414
|
+
end
|
415
|
+
|
416
|
+
time
|
417
|
+
end
|
418
|
+
|
419
|
+
def round_to_seconds(time)
|
420
|
+
# Ruby 1.8 doesn't have #round
|
421
|
+
time.respond_to?(:round) ? time.round : time - time.usec * 1e-6
|
422
|
+
end
|
383
423
|
end
|
384
424
|
end
|
385
425
|
|
data/lib/rufus/scheduler/jobs.rb
CHANGED
@@ -114,7 +114,9 @@ module Rufus
|
|
114
114
|
|
115
115
|
return if opts[:overlap] == false && running?
|
116
116
|
|
117
|
-
r =
|
117
|
+
r =
|
118
|
+
callback(:confirm_lock, time) &&
|
119
|
+
callback(:on_pre_trigger, time)
|
118
120
|
|
119
121
|
return if r == false
|
120
122
|
|
@@ -183,15 +185,14 @@ module Rufus
|
|
183
185
|
|
184
186
|
protected
|
185
187
|
|
186
|
-
def callback(
|
188
|
+
def callback(meth, time)
|
187
189
|
|
188
|
-
|
190
|
+
return true unless @scheduler.respond_to?(meth)
|
189
191
|
|
190
|
-
|
192
|
+
arity = @scheduler.method(meth).arity
|
193
|
+
args = [ self, time ][0, (arity < 0 ? 2 : arity)]
|
191
194
|
|
192
|
-
|
193
|
-
|
194
|
-
@scheduler.send(name, *args)
|
195
|
+
@scheduler.send(meth, *args)
|
195
196
|
end
|
196
197
|
|
197
198
|
def compute_timeout
|
@@ -243,7 +244,7 @@ module Rufus
|
|
243
244
|
|
244
245
|
set_next_time(true, time)
|
245
246
|
|
246
|
-
callback(:
|
247
|
+
callback(:on_post_trigger, time)
|
247
248
|
end
|
248
249
|
|
249
250
|
def start_work_thread
|
data/spec/cronline_spec.rb
CHANGED
@@ -322,6 +322,7 @@ describe Rufus::Scheduler::CronLine do
|
|
322
322
|
pt('* * * * sun', lo(1970, 1, 1)).should == lo(1969, 12, 28, 23, 59, 00)
|
323
323
|
pt('* * 13 * *', lo(1970, 1, 1)).should == lo(1969, 12, 13, 23, 59, 00)
|
324
324
|
pt('0 12 13 * *', lo(1970, 1, 1)).should == lo(1969, 12, 13, 12, 00)
|
325
|
+
pt('0 0 2 1 *', lo(1970, 1, 1)).should == lo(1969, 1, 2, 0, 00)
|
325
326
|
|
326
327
|
pt('* * * * * sun', lo(1970, 1, 1)).should == lo(1969, 12, 28, 23, 59, 59)
|
327
328
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specifying rufus-scheduler
|
4
|
+
#
|
5
|
+
# Fri Nov 1 05:56:03 JST 2013
|
6
|
+
#
|
7
|
+
# Ishinomaki
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'spec_helper'
|
11
|
+
|
12
|
+
|
13
|
+
describe Rufus::Scheduler do
|
14
|
+
|
15
|
+
class LosingLockScheduler < Rufus::Scheduler
|
16
|
+
|
17
|
+
attr_reader :counter
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
super
|
21
|
+
@counter = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def confirm_lock
|
25
|
+
@counter = @counter + 1
|
26
|
+
false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'custom locks' do
|
31
|
+
|
32
|
+
it 'does not trigger when #confirm_lock returns false' do
|
33
|
+
|
34
|
+
s = LosingLockScheduler.new
|
35
|
+
|
36
|
+
count = 0
|
37
|
+
|
38
|
+
s.in('0s') { count = count + 1 }
|
39
|
+
|
40
|
+
sleep 0.7
|
41
|
+
|
42
|
+
count.should == 0
|
43
|
+
s.counter.should == 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
data/spec/scheduler_spec.rb
CHANGED
@@ -693,6 +693,21 @@ describe Rufus::Scheduler do
|
|
693
693
|
end
|
694
694
|
end
|
695
695
|
|
696
|
+
describe '#down?' do
|
697
|
+
|
698
|
+
it 'returns true when the scheduler is down' do
|
699
|
+
|
700
|
+
@scheduler.shutdown
|
701
|
+
|
702
|
+
@scheduler.down?.should == true
|
703
|
+
end
|
704
|
+
|
705
|
+
it 'returns false when the scheduler is up' do
|
706
|
+
|
707
|
+
@scheduler.down?.should == false
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
696
711
|
#--
|
697
712
|
# job methods
|
698
713
|
#++
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rufus-scheduler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0.
|
4
|
+
version: 3.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-12-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: tzinfo
|
@@ -91,6 +91,7 @@ files:
|
|
91
91
|
- spec/job_in_spec.rb
|
92
92
|
- spec/parse_spec.rb
|
93
93
|
- spec/scheduler_spec.rb
|
94
|
+
- spec/custom_locks_spec.rb
|
94
95
|
- spec/job_cron_spec.rb
|
95
96
|
- spec/job_every_spec.rb
|
96
97
|
- rufus-scheduler.gemspec
|
@@ -102,7 +103,7 @@ files:
|
|
102
103
|
homepage: http://github.com/jmettraux/rufus-scheduler
|
103
104
|
licenses:
|
104
105
|
- MIT
|
105
|
-
post_install_message: ! "\n***\n\nThanks for installing rufus-scheduler 3.0.
|
106
|
+
post_install_message: ! "\n***\n\nThanks for installing rufus-scheduler 3.0.3\n\nIt
|
106
107
|
might not be 100% compatible with rufus-scheduler 2.x.\n\nIf you encounter issues
|
107
108
|
with this new rufus-scheduler, especially\nif your app worked fine with previous
|
108
109
|
versions of it, you can\n\nA) Forget it and peg your Gemfile to rufus-scheduler
|