rufus-scheduler 3.0.2 → 3.0.3
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.
- 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
|