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.
@@ -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
@@ -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.
@@ -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.2'
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
- consider_lockfile || return
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
- @lockfile.flock(File::LOCK_UN) if @lockfile
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
- def consider_lockfile
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 = @timezone ? @timezone.utc_to_local(from.getutc) : from
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
- if @timezone
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
- # looks back by slices of two hours,
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 = current = from - 2 * 3600
170
- result = nil
159
+ # start at the previous second
160
+ time = time - 1
171
161
 
172
162
  loop do
173
- nex = next_time(current)
174
- return (result ? result : previous_time(start)) if nex > from
175
- result = current = nex
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
- # never reached
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
 
@@ -114,7 +114,9 @@ module Rufus
114
114
 
115
115
  return if opts[:overlap] == false && running?
116
116
 
117
- r = callback(:pre, time)
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(position, time)
188
+ def callback(meth, time)
187
189
 
188
- name = position == :pre ? :on_pre_trigger : :on_post_trigger
190
+ return true unless @scheduler.respond_to?(meth)
189
191
 
190
- return unless @scheduler.respond_to?(name)
192
+ arity = @scheduler.method(meth).arity
193
+ args = [ self, time ][0, (arity < 0 ? 2 : arity)]
191
194
 
192
- args = @scheduler.method(name).arity < 2 ? [ self ] : [ self, time ]
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(:post, time)
247
+ callback(:on_post_trigger, time)
247
248
  end
248
249
 
249
250
  def start_work_thread
@@ -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
+
@@ -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.2
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-10-21 00:00:00.000000000 Z
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.2\n\nIt
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