rufus-scheduler 3.0.0 → 3.0.9

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.
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2006-2014, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -31,9 +31,9 @@ module Rufus
31
31
  # time and string methods
32
32
  #++
33
33
 
34
- def self.parse(o)
34
+ def self.parse(o, opts={})
35
35
 
36
- opts = { :no_error => true }
36
+ opts[:no_error] = true
37
37
 
38
38
  parse_cron(o, opts) ||
39
39
  parse_in(o, opts) || # covers 'every' schedule strings
@@ -46,12 +46,15 @@ module Rufus
46
46
  o.is_a?(String) ? parse_duration(o, opts) : o
47
47
  end
48
48
 
49
- TZ_REGEX = /\b((?:[a-zA-Z][a-zA-z0-9\-+]+)(?:\/[a-zA-Z0-9\-+]+)?)\b/
49
+ TZ_REGEX = /\b((?:[a-zA-Z][a-zA-z0-9\-+]+)(?:\/[a-zA-Z0-9_\-+]+)?)\b/
50
50
 
51
51
  def self.parse_at(o, opts={})
52
52
 
53
53
  return o if o.is_a?(Time)
54
54
 
55
+ # TODO: deal with tz if suffixed to Chronic string?
56
+ return Chronic.parse(o, opts) if defined?(Chronic)
57
+
55
58
  tz = nil
56
59
  s =
57
60
  o.to_s.gsub(TZ_REGEX) { |m|
@@ -68,9 +71,7 @@ module Rufus
68
71
 
69
72
  t = Time.parse(s)
70
73
 
71
- t = tz.local_to_utc(t) if tz
72
-
73
- t
74
+ tz ? tz.local_to_utc(t) : t
74
75
 
75
76
  rescue StandardError => se
76
77
 
@@ -133,17 +134,17 @@ module Rufus
133
134
  #
134
135
  # Some examples:
135
136
  #
136
- # Rufus::Scheduler.parse_duration_string "0.5" # => 0.5
137
- # Rufus::Scheduler.parse_duration_string "500" # => 0.5
138
- # Rufus::Scheduler.parse_duration_string "1000" # => 1.0
139
- # Rufus::Scheduler.parse_duration_string "1h" # => 3600.0
140
- # Rufus::Scheduler.parse_duration_string "1h10s" # => 3610.0
141
- # Rufus::Scheduler.parse_duration_string "1w2d" # => 777600.0
137
+ # Rufus::Scheduler.parse_duration "0.5" # => 0.5
138
+ # Rufus::Scheduler.parse_duration "500" # => 0.5
139
+ # Rufus::Scheduler.parse_duration "1000" # => 1.0
140
+ # Rufus::Scheduler.parse_duration "1h" # => 3600.0
141
+ # Rufus::Scheduler.parse_duration "1h10s" # => 3610.0
142
+ # Rufus::Scheduler.parse_duration "1w2d" # => 777600.0
142
143
  #
143
144
  # Negative time strings are OK (Thanks Danny Fullerton):
144
145
  #
145
- # Rufus::Scheduler.parse_duration_string "-0.5" # => -0.5
146
- # Rufus::Scheduler.parse_duration_string "-1h" # => -3600.0
146
+ # Rufus::Scheduler.parse_duration "-0.5" # => -0.5
147
+ # Rufus::Scheduler.parse_duration "-1h" # => -3600.0
147
148
  #
148
149
  def self.parse_duration(string, opts={})
149
150
 
@@ -183,13 +184,22 @@ module Rufus
183
184
  mod * val
184
185
  end
185
186
 
187
+ class << self
188
+ #-
189
+ # for compatibility with rufus-scheduler 2.x
190
+ #+
191
+ alias parse_duration_string parse_duration
192
+ alias parse_time_string parse_duration
193
+ end
194
+
195
+
186
196
  # Turns a number of seconds into a a time string
187
197
  #
188
- # Rufus.to_duration_string 0 # => '0s'
189
- # Rufus.to_duration_string 60 # => '1m'
190
- # Rufus.to_duration_string 3661 # => '1h1m1s'
191
- # Rufus.to_duration_string 7 * 24 * 3600 # => '1w'
192
- # Rufus.to_duration_string 30 * 24 * 3600 + 1 # => "4w2d1s"
198
+ # Rufus.to_duration 0 # => '0s'
199
+ # Rufus.to_duration 60 # => '1m'
200
+ # Rufus.to_duration 3661 # => '1h1m1s'
201
+ # Rufus.to_duration 7 * 24 * 3600 # => '1w'
202
+ # Rufus.to_duration 30 * 24 * 3600 + 1 # => "4w2d1s"
193
203
  #
194
204
  # It goes from seconds to the year. Months are not counted (as they
195
205
  # are of variable length). Weeks are counted.
@@ -197,16 +207,14 @@ module Rufus
197
207
  # For 30 days months to be counted, the second parameter of this
198
208
  # method can be set to true.
199
209
  #
200
- # Rufus.to_time_string 30 * 24 * 3600 + 1, true # => "1M1s"
201
- #
202
- # (to_time_string is an alias for to_duration_string)
210
+ # Rufus.to_duration 30 * 24 * 3600 + 1, true # => "1M1s"
203
211
  #
204
212
  # If a Float value is passed, milliseconds will be displayed without
205
213
  # 'marker'
206
214
  #
207
- # Rufus.to_duration_string 0.051 # => "51"
208
- # Rufus.to_duration_string 7.051 # => "7s51"
209
- # Rufus.to_duration_string 0.120 + 30 * 24 * 3600 + 1 # => "4w2d1s120"
215
+ # Rufus.to_duration 0.051 # => "51"
216
+ # Rufus.to_duration 7.051 # => "7s51"
217
+ # Rufus.to_duration 0.120 + 30 * 24 * 3600 + 1 # => "4w2d1s120"
210
218
  #
211
219
  # (this behaviour mirrors the one found for parse_time_string()).
212
220
  #
@@ -238,7 +246,11 @@ module Rufus
238
246
  end
239
247
 
240
248
  class << self
249
+ #-
250
+ # for compatibility with rufus-scheduler 2.x
251
+ #+
241
252
  alias to_duration_string to_duration
253
+ alias to_time_string to_duration
242
254
  end
243
255
 
244
256
  # Turns a number of seconds (integer or Float) into a hash like in :
@@ -250,8 +262,7 @@ module Rufus
250
262
  # Rufus.to_duration_hash 0.120 + 30 * 24 * 3600 + 1
251
263
  # # => { :w => 4, :d => 2, :s => 1, :ms => "120" }
252
264
  #
253
- # This method is used by to_duration_string (to_time_string) behind
254
- # the scene.
265
+ # This method is used by to_duration behind the scenes.
255
266
  #
256
267
  # Options are :
257
268
  #
@@ -1,5 +1,5 @@
1
1
  #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
2
+ # Copyright (c) 2006-2014, John Mettraux, jmettraux@gmail.com
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining a copy
5
5
  # of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +26,6 @@ require 'date' if RUBY_VERSION < '1.9.0'
26
26
  require 'time'
27
27
  require 'thread'
28
28
  require 'tzinfo'
29
- require 'fileutils'
30
29
 
31
30
 
32
31
  module Rufus
@@ -37,16 +36,28 @@ module Rufus
37
36
  require 'rufus/scheduler/jobs'
38
37
  require 'rufus/scheduler/cronline'
39
38
  require 'rufus/scheduler/job_array'
39
+ require 'rufus/scheduler/locks'
40
40
 
41
- VERSION = '3.0.0'
41
+ VERSION = '3.0.9'
42
+
43
+ #
44
+ # A common error class for rufus-scheduler
45
+ #
46
+ class Error < StandardError; end
42
47
 
43
48
  #
44
49
  # This error is thrown when the :timeout attribute triggers
45
50
  #
46
- class TimeoutError < StandardError; end
51
+ class TimeoutError < Error; end
52
+
53
+ #
54
+ # For when the scheduler is not running
55
+ # (it got shut down or didn't start because of a lock)
56
+ #
57
+ class NotRunningError < Error; end
47
58
 
48
- #MIN_WORK_THREADS = 7
49
- MAX_WORK_THREADS = 35
59
+ #MIN_WORK_THREADS = 3
60
+ MAX_WORK_THREADS = 28
50
61
 
51
62
  attr_accessor :frequency
52
63
  attr_reader :started_at
@@ -82,7 +93,17 @@ module Rufus
82
93
 
83
94
  @thread_key = "rufus_scheduler_#{self.object_id}"
84
95
 
85
- consider_lockfile || return
96
+ @scheduler_lock =
97
+ if lockfile = opts[:lockfile]
98
+ Rufus::Scheduler::FileLock.new(lockfile)
99
+ else
100
+ opts[:scheduler_lock] || Rufus::Scheduler::NullLock.new
101
+ end
102
+
103
+ @trigger_lock = opts[:trigger_lock] || Rufus::Scheduler::NullLock.new
104
+
105
+ # If we can't grab the @scheduler_lock, don't run.
106
+ @scheduler_lock.lock || return
86
107
 
87
108
  start
88
109
  end
@@ -113,7 +134,9 @@ module Rufus
113
134
 
114
135
  @started_at = nil
115
136
 
116
- jobs.each { |j| j.unschedule }
137
+ #jobs.each { |j| j.unschedule }
138
+ # provokes https://github.com/jmettraux/rufus-scheduler/issue/98
139
+ @jobs.array.each { |j| j.unschedule }
117
140
 
118
141
  @work_queue.clear
119
142
 
@@ -123,7 +146,7 @@ module Rufus
123
146
  kill_all_work_threads
124
147
  end
125
148
 
126
- @lockfile.flock(File::LOCK_UN) if @lockfile
149
+ unlock
127
150
  end
128
151
 
129
152
  alias stop shutdown
@@ -140,9 +163,23 @@ module Rufus
140
163
 
141
164
  def join
142
165
 
166
+ fail NotRunningError.new(
167
+ 'cannot join scheduler that is not running'
168
+ ) unless @thread
169
+
143
170
  @thread.join
144
171
  end
145
172
 
173
+ def down?
174
+
175
+ ! @started_at
176
+ end
177
+
178
+ def up?
179
+
180
+ !! @started_at
181
+ end
182
+
146
183
  def paused?
147
184
 
148
185
  @paused
@@ -214,9 +251,9 @@ module Rufus
214
251
 
215
252
  def schedule(arg, callable=nil, opts={}, &block)
216
253
 
217
- # TODO: eventually, spare one parse call
254
+ opts[:_t] = Scheduler.parse(arg, opts)
218
255
 
219
- case Scheduler.parse(arg)
256
+ case opts[:_t]
220
257
  when CronLine then schedule_cron(arg, callable, opts, &block)
221
258
  when Time then schedule_at(arg, callable, opts, &block)
222
259
  else schedule_in(arg, callable, opts, &block)
@@ -225,9 +262,9 @@ module Rufus
225
262
 
226
263
  def repeat(arg, callable=nil, opts={}, &block)
227
264
 
228
- # TODO: eventually, spare one parse call
265
+ opts[:_t] = Scheduler.parse(arg, opts)
229
266
 
230
- case Scheduler.parse(arg)
267
+ case opts[:_t]
231
268
  when CronLine then schedule_cron(arg, callable, opts, &block)
232
269
  else schedule_every(arg, callable, opts, &block)
233
270
  end
@@ -297,6 +334,49 @@ module Rufus
297
334
  @jobs[job_id]
298
335
  end
299
336
 
337
+ # Returns true if the scheduler has acquired the [exclusive] lock and
338
+ # thus may run.
339
+ #
340
+ # Most of the time, a scheduler is run alone and this method should
341
+ # return true. It is useful in cases where among a group of applications
342
+ # only one of them should run the scheduler. For schedulers that should
343
+ # not run, the method should return false.
344
+ #
345
+ # Out of the box, rufus-scheduler proposes the
346
+ # :lockfile => 'path/to/lock/file' scheduler start option. It makes
347
+ # it easy for schedulers on the same machine to determine which should
348
+ # run (the first to write the lockfile and lock it). It uses "man 2 flock"
349
+ # so it probably won't work reliably on distributed file systems.
350
+ #
351
+ # If one needs to use a special/different locking mechanism, the scheduler
352
+ # accepts :scheduler_lock => lock_object. lock_object only needs to respond
353
+ # to #lock
354
+ # and #unlock, and both of these methods should be idempotent.
355
+ #
356
+ # Look at rufus/scheduler/locks.rb for an example.
357
+ #
358
+ def lock
359
+
360
+ @scheduler_lock.lock
361
+ end
362
+
363
+ # Sister method to #lock, is called when the scheduler shuts down.
364
+ #
365
+ def unlock
366
+
367
+ @trigger_lock.unlock
368
+ @scheduler_lock.unlock
369
+ end
370
+
371
+ # Callback called when a job is triggered. If the lock cannot be acquired,
372
+ # the job won't run (though it'll still be scheduled to run again if
373
+ # necessary).
374
+ #
375
+ def confirm_lock
376
+
377
+ @trigger_lock.lock
378
+ end
379
+
300
380
  # Returns true if this job is currently scheduled.
301
381
  #
302
382
  # Takes extra care to answer true if the job is a repeat job
@@ -348,13 +428,39 @@ module Rufus
348
428
  jobs(opts.merge(:running => true))
349
429
  end
350
430
 
431
+ def occurrences(time0, time1, format=:per_job)
432
+
433
+ h = {}
434
+
435
+ jobs.each do |j|
436
+ os = j.occurrences(time0, time1)
437
+ h[j] = os if os.any?
438
+ end
439
+
440
+ if format == :timeline
441
+ a = []
442
+ h.each { |j, ts| ts.each { |t| a << [ t, j ] } }
443
+ a.sort_by { |(t, j)| t }
444
+ else
445
+ h
446
+ end
447
+ end
448
+
449
+ def timeline(time0, time1)
450
+
451
+ occurrences(time0, time1, :timeline)
452
+ end
453
+
351
454
  def on_error(job, err)
352
455
 
353
456
  pre = err.object_id.to_s
354
457
 
458
+ ms = {}; mutexes.each { |k, v| ms[k] = v.locked? }
459
+
355
460
  stderr.puts("{ #{pre} rufus-scheduler intercepted an error:")
356
461
  stderr.puts(" #{pre} job:")
357
462
  stderr.puts(" #{pre} #{job.class} #{job.original.inspect} #{job.opts.inspect}")
463
+ # TODO: eventually use a Job#detail or something like that
358
464
  stderr.puts(" #{pre} error:")
359
465
  stderr.puts(" #{pre} #{err.object_id}")
360
466
  stderr.puts(" #{pre} #{err.class}")
@@ -362,6 +468,34 @@ module Rufus
362
468
  err.backtrace.each do |l|
363
469
  stderr.puts(" #{pre} #{l}")
364
470
  end
471
+ stderr.puts(" #{pre} tz:")
472
+ stderr.puts(" #{pre} ENV['TZ']: #{ENV['TZ']}")
473
+ stderr.puts(" #{pre} Time.now: #{Time.now}")
474
+ stderr.puts(" #{pre} scheduler:")
475
+ stderr.puts(" #{pre} object_id: #{object_id}")
476
+ stderr.puts(" #{pre} opts:")
477
+ stderr.puts(" #{pre} #{@opts.inspect}")
478
+ stderr.puts(" #{pre} frequency: #{self.frequency}")
479
+ stderr.puts(" #{pre} scheduler_lock: #{@scheduler_lock.inspect}")
480
+ stderr.puts(" #{pre} trigger_lock: #{@trigger_lock.inspect}")
481
+ stderr.puts(" #{pre} uptime: #{uptime} (#{uptime_s})")
482
+ stderr.puts(" #{pre} down?: #{down?}")
483
+ stderr.puts(" #{pre} threads: #{self.threads.size}")
484
+ stderr.puts(" #{pre} thread: #{self.thread}")
485
+ stderr.puts(" #{pre} thread_key: #{self.thread_key}")
486
+ stderr.puts(" #{pre} work_threads: #{work_threads.size}")
487
+ stderr.puts(" #{pre} active: #{work_threads(:active).size}")
488
+ stderr.puts(" #{pre} vacant: #{work_threads(:vacant).size}")
489
+ stderr.puts(" #{pre} max_work_threads: #{max_work_threads}")
490
+ stderr.puts(" #{pre} mutexes: #{ms.inspect}")
491
+ stderr.puts(" #{pre} jobs: #{jobs.size}")
492
+ stderr.puts(" #{pre} at_jobs: #{at_jobs.size}")
493
+ stderr.puts(" #{pre} in_jobs: #{in_jobs.size}")
494
+ stderr.puts(" #{pre} every_jobs: #{every_jobs.size}")
495
+ stderr.puts(" #{pre} interval_jobs: #{interval_jobs.size}")
496
+ stderr.puts(" #{pre} cron_jobs: #{cron_jobs.size}")
497
+ stderr.puts(" #{pre} running_jobs: #{running_jobs.size}")
498
+ stderr.puts(" #{pre} work_queue: #{work_queue.size}")
365
499
  stderr.puts("} #{pre} .")
366
500
 
367
501
  rescue => e
@@ -388,36 +522,6 @@ module Rufus
388
522
  end
389
523
  end
390
524
 
391
- def consider_lockfile
392
-
393
- @lockfile = nil
394
-
395
- return true unless f = @opts[:lockfile]
396
-
397
- raise ArgumentError.new(
398
- ":lockfile argument must be a string, not a #{f.class}"
399
- ) unless f.is_a?(String)
400
-
401
- FileUtils.mkdir_p(File.dirname(f))
402
-
403
- f = File.new(f, File::RDWR | File::CREAT)
404
- locked = f.flock(File::LOCK_NB | File::LOCK_EX)
405
-
406
- return false unless locked
407
-
408
- now = Time.now
409
-
410
- f.print("pid: #{$$}, ")
411
- f.print("scheduler.object_id: #{self.object_id}, ")
412
- f.print("time: #{now}, ")
413
- f.print("timestamp: #{now.to_f}")
414
- f.flush
415
-
416
- @lockfile = f
417
-
418
- true
419
- end
420
-
421
525
  def terminate_all_jobs
422
526
 
423
527
  jobs.each { |j| j.unschedule }
@@ -502,7 +606,7 @@ module Rufus
502
606
 
503
607
  def do_schedule(job_type, t, callable, opts, return_job_instance, block)
504
608
 
505
- raise RuntimeError.new(
609
+ fail NotRunningError.new(
506
610
  'cannot schedule, scheduler is down or shutting down'
507
611
  ) if @started_at == nil
508
612
 
@@ -512,8 +616,8 @@ module Rufus
512
616
  job_class =
513
617
  case job_type
514
618
  when :once
515
- tt = Rufus::Scheduler.parse(t)
516
- tt.is_a?(Time) ? AtJob : InJob
619
+ opts[:_t] ||= Rufus::Scheduler.parse(t, opts)
620
+ opts[:_t].is_a?(Time) ? AtJob : InJob
517
621
  when :every
518
622
  EveryJob
519
623
  when :interval
@@ -30,6 +30,7 @@ job scheduler for Ruby (at, cron, in and every jobs).
30
30
 
31
31
  s.add_development_dependency 'rake'
32
32
  s.add_development_dependency 'rspec', '>= 2.13.0'
33
+ s.add_development_dependency 'chronic'
33
34
 
34
35
  s.require_path = 'lib'
35
36
 
@@ -49,7 +50,7 @@ A) Forget it and peg your Gemfile to rufus-scheduler 2.0.24
49
50
  and / or
50
51
 
51
52
  B) Take some time to carefully report the issue at
52
- https://github.com/jmettraux/rufus-scheduler/issue
53
+ https://github.com/jmettraux/rufus-scheduler/issues
53
54
 
54
55
  For general help about rufus-scheduler, ask via:
55
56
  http://stackoverflow.com/questions/ask?tags=rufus-scheduler+ruby
@@ -0,0 +1,54 @@
1
+
2
+ #
3
+ # Specifying rufus-scheduler
4
+ #
5
+ # Sun Jun 1 05:52:24 JST 2014
6
+ #
7
+
8
+ require 'spec_helper'
9
+
10
+
11
+ describe 'basics' do
12
+
13
+ def tts(time)
14
+
15
+ time.strftime('%Y-%m-%d %H:%M:%S %z') + (time.dst? ? ' dst' : '')
16
+ end
17
+
18
+ describe 'Time.new' do
19
+
20
+ it 'accepts a timezone final argument' do
21
+
22
+ if jruby? or ruby18?
23
+
24
+ expect(true).to be(true)
25
+
26
+ else
27
+
28
+ expect(
29
+ tts(Time.new(2014, 1, 1, 1, 0, 0, '+01:00'))
30
+ ).to eq('2014-01-01 01:00:00 +0100')
31
+ expect(
32
+ tts(Time.new(2014, 8, 1, 1, 0, 0, '+01:00'))
33
+ ).to eq('2014-08-01 01:00:00 +0100')
34
+ expect(
35
+ tts(Time.new(2014, 8, 1, 1, 0, 0, '+01:00'))
36
+ ).to eq('2014-08-01 01:00:00 +0100')
37
+ end
38
+ end
39
+ end
40
+
41
+ describe 'Time.local' do
42
+
43
+ it 'works as expected' do
44
+
45
+ expect(
46
+ tts(in_zone('Europe/Berlin') { Time.local(2014, 1, 1, 1, 0, 0) })
47
+ ).to eq('2014-01-01 01:00:00 +0100')
48
+ expect(
49
+ tts(in_zone('Europe/Berlin') { Time.local(2014, 8, 1, 1, 0, 0) })
50
+ ).to eq('2014-08-01 01:00:00 +0200 dst')
51
+ end
52
+ end
53
+ end
54
+