rufus-scheduler 3.1.4 → 3.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +410 -0
  3. data/CREDITS.md +142 -0
  4. data/LICENSE.txt +1 -1
  5. data/Makefile +27 -0
  6. data/README.md +407 -143
  7. data/lib/rufus/scheduler/job_array.rb +36 -66
  8. data/lib/rufus/scheduler/jobs_core.rb +369 -0
  9. data/lib/rufus/scheduler/jobs_one_time.rb +53 -0
  10. data/lib/rufus/scheduler/jobs_repeat.rb +335 -0
  11. data/lib/rufus/scheduler/locks.rb +41 -67
  12. data/lib/rufus/scheduler/util.rb +89 -179
  13. data/lib/rufus/scheduler.rb +545 -453
  14. data/rufus-scheduler.gemspec +22 -11
  15. metadata +44 -85
  16. data/CHANGELOG.txt +0 -243
  17. data/CREDITS.txt +0 -88
  18. data/Rakefile +0 -83
  19. data/TODO.txt +0 -151
  20. data/lib/rufus/scheduler/cronline.rb +0 -470
  21. data/lib/rufus/scheduler/jobs.rb +0 -633
  22. data/lib/rufus/scheduler/zones.rb +0 -174
  23. data/lib/rufus/scheduler/zotime.rb +0 -155
  24. data/spec/basics_spec.rb +0 -54
  25. data/spec/cronline_spec.rb +0 -915
  26. data/spec/error_spec.rb +0 -139
  27. data/spec/job_array_spec.rb +0 -39
  28. data/spec/job_at_spec.rb +0 -58
  29. data/spec/job_cron_spec.rb +0 -128
  30. data/spec/job_every_spec.rb +0 -104
  31. data/spec/job_in_spec.rb +0 -20
  32. data/spec/job_interval_spec.rb +0 -68
  33. data/spec/job_repeat_spec.rb +0 -357
  34. data/spec/job_spec.rb +0 -631
  35. data/spec/lock_custom_spec.rb +0 -47
  36. data/spec/lock_flock_spec.rb +0 -47
  37. data/spec/lock_lockfile_spec.rb +0 -61
  38. data/spec/lock_spec.rb +0 -59
  39. data/spec/parse_spec.rb +0 -263
  40. data/spec/schedule_at_spec.rb +0 -158
  41. data/spec/schedule_cron_spec.rb +0 -66
  42. data/spec/schedule_every_spec.rb +0 -109
  43. data/spec/schedule_in_spec.rb +0 -80
  44. data/spec/schedule_interval_spec.rb +0 -128
  45. data/spec/scheduler_spec.rb +0 -1067
  46. data/spec/spec_helper.rb +0 -126
  47. data/spec/threads_spec.rb +0 -96
  48. data/spec/zotime_spec.rb +0 -396
@@ -1,643 +1,735 @@
1
- #--
2
- # Copyright (c) 2006-2015, John Mettraux, jmettraux@gmail.com
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining a copy
5
- # of this software and associated documentation files (the "Software"), to deal
6
- # in the Software without restriction, including without limitation the rights
7
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the Software is
9
- # furnished to do so, subject to the following conditions:
10
- #
11
- # The above copyright notice and this permission notice shall be included in
12
- # all copies or substantial portions of the Software.
13
- #
14
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
- # THE SOFTWARE.
21
- #
22
- # Made in Japan.
23
- #++
24
1
 
25
2
  require 'date' if RUBY_VERSION < '1.9.0'
26
- require 'time'
27
3
  require 'thread'
28
- #require 'tzinfo'
29
4
 
5
+ require 'fugit'
30
6
 
31
- module Rufus
32
7
 
33
- class Scheduler
8
+ module Rufus; end
34
9
 
35
- require 'rufus/scheduler/util'
36
- require 'rufus/scheduler/zotime'
37
- require 'rufus/scheduler/jobs'
38
- require 'rufus/scheduler/cronline'
39
- require 'rufus/scheduler/job_array'
40
- require 'rufus/scheduler/locks'
10
+ class Rufus::Scheduler
41
11
 
42
- VERSION = '3.1.4'
12
+ VERSION = '3.8.2'
43
13
 
44
- #
45
- # A common error class for rufus-scheduler
46
- #
47
- class Error < StandardError; end
14
+ EoTime = ::EtOrbi::EoTime
48
15
 
49
- #
50
- # This error is thrown when the :timeout attribute triggers
51
- #
52
- class TimeoutError < Error; end
16
+ require 'rufus/scheduler/util'
17
+ require 'rufus/scheduler/jobs_core'
18
+ require 'rufus/scheduler/jobs_one_time'
19
+ require 'rufus/scheduler/jobs_repeat'
20
+ require 'rufus/scheduler/job_array'
21
+ require 'rufus/scheduler/locks'
53
22
 
54
- #
55
- # For when the scheduler is not running
56
- # (it got shut down or didn't start because of a lock)
57
- #
58
- class NotRunningError < Error; end
23
+ #
24
+ # A common error class for rufus-scheduler
25
+ #
26
+ class Error < StandardError; end
59
27
 
60
- #MIN_WORK_THREADS = 3
61
- MAX_WORK_THREADS = 28
28
+ #
29
+ # This error is thrown when the :timeout attribute triggers
30
+ #
31
+ class TimeoutError < Error; end
62
32
 
63
- attr_accessor :frequency
64
- attr_reader :started_at
65
- attr_reader :thread
66
- attr_reader :thread_key
67
- attr_reader :mutexes
33
+ #
34
+ # For when the scheduler is not running
35
+ # (it got shut down or didn't start because of a lock)
36
+ #
37
+ class NotRunningError < Error; end
68
38
 
69
- #attr_accessor :min_work_threads
70
- attr_accessor :max_work_threads
39
+ #MIN_WORK_THREADS = 3
40
+ MAX_WORK_THREADS = 28
71
41
 
72
- attr_accessor :stderr
42
+ attr_accessor :frequency
43
+ attr_accessor :discard_past
73
44
 
74
- attr_reader :work_queue
45
+ attr_reader :started_at
46
+ attr_reader :paused_at
47
+ attr_reader :thread
48
+ attr_reader :thread_key
49
+ attr_reader :mutexes
75
50
 
76
- def initialize(opts={})
51
+ #attr_accessor :min_work_threads
52
+ attr_accessor :max_work_threads
77
53
 
78
- @opts = opts
54
+ attr_accessor :stderr
79
55
 
80
- @started_at = nil
81
- @paused = false
56
+ attr_reader :work_queue
82
57
 
83
- @jobs = JobArray.new
58
+ def initialize(opts={})
84
59
 
85
- @frequency = Rufus::Scheduler.parse(opts[:frequency] || 0.300)
86
- @mutexes = {}
60
+ @opts = opts
87
61
 
88
- @work_queue = Queue.new
62
+ @started_at = nil
63
+ @paused_at = nil
89
64
 
90
- #@min_work_threads = opts[:min_work_threads] || MIN_WORK_THREADS
91
- @max_work_threads = opts[:max_work_threads] || MAX_WORK_THREADS
65
+ @jobs = JobArray.new
92
66
 
93
- @stderr = $stderr
67
+ @frequency = Rufus::Scheduler.parse(opts[:frequency] || 0.300)
68
+ @discard_past = opts.has_key?(:discard_past) ? opts[:discard_past] : true
94
69
 
95
- @thread_key = "rufus_scheduler_#{self.object_id}"
70
+ @mutexes = {}
96
71
 
97
- @scheduler_lock =
98
- if lockfile = opts[:lockfile]
99
- Rufus::Scheduler::FileLock.new(lockfile)
100
- else
101
- opts[:scheduler_lock] || Rufus::Scheduler::NullLock.new
102
- end
103
-
104
- @trigger_lock = opts[:trigger_lock] || Rufus::Scheduler::NullLock.new
72
+ @work_queue = Queue.new
73
+ @join_queue = Queue.new
105
74
 
106
- # If we can't grab the @scheduler_lock, don't run.
107
- @scheduler_lock.lock || return
75
+ #@min_work_threads =
76
+ # opts[:min_work_threads] || opts[:min_worker_threads] ||
77
+ # MIN_WORK_THREADS
78
+ @max_work_threads =
79
+ opts[:max_work_threads] || opts[:max_worker_threads] ||
80
+ MAX_WORK_THREADS
108
81
 
109
- start
110
- end
82
+ @stderr = $stderr
111
83
 
112
- # Returns a singleton Rufus::Scheduler instance
113
- #
114
- def self.singleton(opts={})
84
+ @thread_key = "rufus_scheduler_#{self.object_id}"
115
85
 
116
- @singleton ||= Rufus::Scheduler.new(opts)
117
- end
86
+ @scheduler_lock =
87
+ if lockfile = opts[:lockfile]
88
+ Rufus::Scheduler::FileLock.new(lockfile)
89
+ else
90
+ opts[:scheduler_lock] || Rufus::Scheduler::NullLock.new
91
+ end
118
92
 
119
- # Alias for Rufus::Scheduler.singleton
120
- #
121
- def self.s(opts={}); singleton(opts); end
93
+ @trigger_lock = opts[:trigger_lock] || Rufus::Scheduler::NullLock.new
122
94
 
123
- # Releasing the gem would probably require redirecting .start_new to
124
- # .new and emit a simple deprecation message.
125
- #
126
- # For now, let's assume the people pointing at rufus-scheduler/master
127
- # on GitHub know what they do...
128
- #
129
- def self.start_new
95
+ # If we can't grab the @scheduler_lock, don't run.
96
+ lock || return
130
97
 
131
- fail "this is rufus-scheduler 3.0, use .new instead of .start_new"
132
- end
98
+ start
99
+ end
133
100
 
134
- def shutdown(opt=nil)
101
+ # Returns a singleton Rufus::Scheduler instance
102
+ #
103
+ def self.singleton(opts={})
135
104
 
136
- @started_at = nil
105
+ @singleton ||= Rufus::Scheduler.new(opts)
106
+ end
137
107
 
138
- #jobs.each { |j| j.unschedule }
139
- # provokes https://github.com/jmettraux/rufus-scheduler/issue/98
140
- @jobs.array.each { |j| j.unschedule }
108
+ # Alias for Rufus::Scheduler.singleton
109
+ #
110
+ def self.s(opts={}); singleton(opts); end
141
111
 
142
- @work_queue.clear
112
+ # Releasing the gem would probably require redirecting .start_new to
113
+ # .new and emit a simple deprecation message.
114
+ #
115
+ # For now, let's assume the people pointing at rufus-scheduler/master
116
+ # on GitHub know what they do...
117
+ #
118
+ def self.start_new
143
119
 
144
- if opt == :wait
145
- join_all_work_threads
146
- elsif opt == :kill
147
- kill_all_work_threads
148
- end
120
+ fail 'this is rufus-scheduler 3.x, use .new instead of .start_new'
121
+ end
149
122
 
150
- unlock
151
- end
123
+ def uptime
152
124
 
153
- alias stop shutdown
125
+ @started_at ? EoTime.now - @started_at : nil
126
+ end
154
127
 
155
- def uptime
128
+ def around_trigger(job)
156
129
 
157
- @started_at ? Time.now - @started_at : nil
158
- end
130
+ yield
131
+ end
159
132
 
160
- def uptime_s
133
+ def uptime_s
161
134
 
162
- self.class.to_duration(uptime)
163
- end
135
+ uptime ? self.class.to_duration(uptime) : ''
136
+ end
164
137
 
165
- def join
138
+ def join(time_limit=nil)
166
139
 
167
- fail NotRunningError.new(
168
- 'cannot join scheduler that is not running'
169
- ) unless @thread
140
+ fail NotRunningError.new('cannot join scheduler that is not running') \
141
+ unless @thread
142
+ fail ThreadError.new('scheduler thread cannot join itself') \
143
+ if @thread == Thread.current
170
144
 
171
- @thread.join
145
+ if time_limit
146
+ time_limit_join(time_limit)
147
+ else
148
+ no_time_limit_join
172
149
  end
150
+ end
173
151
 
174
- def down?
152
+ def down?
175
153
 
176
- ! @started_at
177
- end
178
-
179
- def up?
154
+ ! @started_at
155
+ end
180
156
 
181
- !! @started_at
182
- end
157
+ def up?
183
158
 
184
- def paused?
159
+ !! @started_at
160
+ end
185
161
 
186
- @paused
187
- end
162
+ def paused?
188
163
 
189
- def pause
164
+ !! @paused_at
165
+ end
190
166
 
191
- @paused = true
192
- end
167
+ def pause
193
168
 
194
- def resume
169
+ @paused_at = EoTime.now
170
+ end
195
171
 
196
- @paused = false
197
- end
172
+ def resume(opts={})
198
173
 
199
- #--
200
- # scheduling methods
201
- #++
174
+ dp = opts[:discard_past]
175
+ jobs.each { |job| job.resume_discard_past = dp }
202
176
 
203
- def at(time, callable=nil, opts={}, &block)
177
+ @paused_at = nil
178
+ end
204
179
 
205
- do_schedule(:once, time, callable, opts, opts[:job], block)
206
- end
180
+ #--
181
+ # scheduling methods
182
+ #++
207
183
 
208
- def schedule_at(time, callable=nil, opts={}, &block)
184
+ def at(time, callable=nil, opts={}, &block)
209
185
 
210
- do_schedule(:once, time, callable, opts, true, block)
211
- end
186
+ do_schedule(:once, time, callable, opts, opts[:job], block)
187
+ end
212
188
 
213
- def in(duration, callable=nil, opts={}, &block)
189
+ def schedule_at(time, callable=nil, opts={}, &block)
214
190
 
215
- do_schedule(:once, duration, callable, opts, opts[:job], block)
216
- end
191
+ do_schedule(:once, time, callable, opts, true, block)
192
+ end
217
193
 
218
- def schedule_in(duration, callable=nil, opts={}, &block)
194
+ def in(duration, callable=nil, opts={}, &block)
219
195
 
220
- do_schedule(:once, duration, callable, opts, true, block)
221
- end
196
+ do_schedule(:once, duration, callable, opts, opts[:job], block)
197
+ end
222
198
 
223
- def every(duration, callable=nil, opts={}, &block)
199
+ def schedule_in(duration, callable=nil, opts={}, &block)
224
200
 
225
- do_schedule(:every, duration, callable, opts, opts[:job], block)
226
- end
201
+ do_schedule(:once, duration, callable, opts, true, block)
202
+ end
227
203
 
228
- def schedule_every(duration, callable=nil, opts={}, &block)
204
+ def every(duration, callable=nil, opts={}, &block)
229
205
 
230
- do_schedule(:every, duration, callable, opts, true, block)
231
- end
206
+ do_schedule(:every, duration, callable, opts, opts[:job], block)
207
+ end
232
208
 
233
- def interval(duration, callable=nil, opts={}, &block)
209
+ def schedule_every(duration, callable=nil, opts={}, &block)
234
210
 
235
- do_schedule(:interval, duration, callable, opts, opts[:job], block)
236
- end
211
+ do_schedule(:every, duration, callable, opts, true, block)
212
+ end
237
213
 
238
- def schedule_interval(duration, callable=nil, opts={}, &block)
214
+ def interval(duration, callable=nil, opts={}, &block)
239
215
 
240
- do_schedule(:interval, duration, callable, opts, true, block)
241
- end
216
+ do_schedule(:interval, duration, callable, opts, opts[:job], block)
217
+ end
242
218
 
243
- def cron(cronline, callable=nil, opts={}, &block)
219
+ def schedule_interval(duration, callable=nil, opts={}, &block)
244
220
 
245
- do_schedule(:cron, cronline, callable, opts, opts[:job], block)
246
- end
221
+ do_schedule(:interval, duration, callable, opts, true, block)
222
+ end
247
223
 
248
- def schedule_cron(cronline, callable=nil, opts={}, &block)
224
+ def cron(cronline, callable=nil, opts={}, &block)
249
225
 
250
- do_schedule(:cron, cronline, callable, opts, true, block)
251
- end
226
+ do_schedule(:cron, cronline, callable, opts, opts[:job], block)
227
+ end
252
228
 
253
- def schedule(arg, callable=nil, opts={}, &block)
229
+ def schedule_cron(cronline, callable=nil, opts={}, &block)
254
230
 
255
- opts[:_t] = Scheduler.parse(arg, opts)
231
+ do_schedule(:cron, cronline, callable, opts, true, block)
232
+ end
256
233
 
257
- case opts[:_t]
258
- when CronLine then schedule_cron(arg, callable, opts, &block)
259
- when Time then schedule_at(arg, callable, opts, &block)
260
- else schedule_in(arg, callable, opts, &block)
261
- end
262
- end
234
+ def schedule(arg, callable=nil, opts={}, &block)
263
235
 
264
- def repeat(arg, callable=nil, opts={}, &block)
236
+ callable, opts = nil, callable if callable.is_a?(Hash)
237
+ opts = opts.dup
265
238
 
266
- opts[:_t] = Scheduler.parse(arg, opts)
239
+ opts[:_t] = Rufus::Scheduler.parse(arg, opts)
267
240
 
268
- case opts[:_t]
269
- when CronLine then schedule_cron(arg, callable, opts, &block)
270
- else schedule_every(arg, callable, opts, &block)
271
- end
241
+ case opts[:_t]
242
+ when ::Fugit::Cron then schedule_cron(arg, callable, opts, &block)
243
+ when ::EtOrbi::EoTime, Time then schedule_at(arg, callable, opts, &block)
244
+ else schedule_in(arg, callable, opts, &block)
272
245
  end
246
+ end
273
247
 
274
- def unschedule(job_or_job_id)
248
+ def repeat(arg, callable=nil, opts={}, &block)
275
249
 
276
- job, job_id = fetch(job_or_job_id)
250
+ callable, opts = nil, callable if callable.is_a?(Hash)
251
+ opts = opts.dup
277
252
 
278
- fail ArgumentError.new("no job found with id '#{job_id}'") unless job
253
+ opts[:_t] = Rufus::Scheduler.parse(arg, opts)
279
254
 
280
- job.unschedule if job
255
+ case opts[:_t]
256
+ when ::Fugit::Cron then schedule_cron(arg, callable, opts, &block)
257
+ else schedule_every(arg, callable, opts, &block)
281
258
  end
259
+ end
282
260
 
283
- #--
284
- # jobs methods
285
- #++
261
+ def unschedule(job_or_job_id)
286
262
 
287
- # Returns all the scheduled jobs
288
- # (even those right before re-schedule).
289
- #
290
- def jobs(opts={})
263
+ job, job_id = fetch(job_or_job_id)
291
264
 
292
- opts = { opts => true } if opts.is_a?(Symbol)
265
+ fail ArgumentError.new("no job found with id '#{job_id}'") unless job
293
266
 
294
- jobs = @jobs.to_a
267
+ job.unschedule if job
268
+ end
295
269
 
296
- if opts[:running]
297
- jobs = jobs.select { |j| j.running? }
298
- elsif ! opts[:all]
299
- jobs = jobs.reject { |j| j.next_time.nil? || j.unscheduled_at }
300
- end
270
+ #--
271
+ # jobs methods
272
+ #++
301
273
 
302
- tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
303
- jobs = jobs.reject { |j| tags.find { |t| ! j.tags.include?(t) } }
274
+ # Returns all the scheduled jobs
275
+ # (even those right before re-schedule).
276
+ #
277
+ def jobs(opts={})
304
278
 
305
- jobs
306
- end
279
+ opts = { opts => true } if opts.is_a?(Symbol)
307
280
 
308
- def at_jobs(opts={})
281
+ jobs = @jobs.to_a
309
282
 
310
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::AtJob) }
283
+ if opts[:running]
284
+ jobs = jobs.select { |j| j.running? }
285
+ elsif ! opts[:all]
286
+ jobs = jobs.reject { |j| j.next_time.nil? || j.unscheduled_at }
311
287
  end
312
288
 
313
- def in_jobs(opts={})
289
+ tags = Array(opts[:tag] || opts[:tags]).collect(&:to_s)
290
+ jobs = jobs.reject { |j| tags.find { |t| ! j.tags.include?(t) } }
314
291
 
315
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::InJob) }
316
- end
292
+ jobs
293
+ end
317
294
 
318
- def every_jobs(opts={})
295
+ def at_jobs(opts={})
319
296
 
320
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::EveryJob) }
321
- end
297
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::AtJob) }
298
+ end
322
299
 
323
- def interval_jobs(opts={})
300
+ def in_jobs(opts={})
324
301
 
325
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::IntervalJob) }
326
- end
302
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::InJob) }
303
+ end
327
304
 
328
- def cron_jobs(opts={})
305
+ def every_jobs(opts={})
329
306
 
330
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::CronJob) }
331
- end
307
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::EveryJob) }
308
+ end
332
309
 
333
- def job(job_id)
310
+ def interval_jobs(opts={})
334
311
 
335
- @jobs[job_id]
336
- end
312
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::IntervalJob) }
313
+ end
337
314
 
338
- # Returns true if the scheduler has acquired the [exclusive] lock and
339
- # thus may run.
340
- #
341
- # Most of the time, a scheduler is run alone and this method should
342
- # return true. It is useful in cases where among a group of applications
343
- # only one of them should run the scheduler. For schedulers that should
344
- # not run, the method should return false.
345
- #
346
- # Out of the box, rufus-scheduler proposes the
347
- # :lockfile => 'path/to/lock/file' scheduler start option. It makes
348
- # it easy for schedulers on the same machine to determine which should
349
- # run (the first to write the lockfile and lock it). It uses "man 2 flock"
350
- # so it probably won't work reliably on distributed file systems.
351
- #
352
- # If one needs to use a special/different locking mechanism, the scheduler
353
- # accepts :scheduler_lock => lock_object. lock_object only needs to respond
354
- # to #lock
355
- # and #unlock, and both of these methods should be idempotent.
356
- #
357
- # Look at rufus/scheduler/locks.rb for an example.
358
- #
359
- def lock
360
-
361
- @scheduler_lock.lock
362
- end
315
+ def cron_jobs(opts={})
363
316
 
364
- # Sister method to #lock, is called when the scheduler shuts down.
365
- #
366
- def unlock
317
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::CronJob) }
318
+ end
367
319
 
368
- @trigger_lock.unlock
369
- @scheduler_lock.unlock
370
- end
320
+ def job(job_id)
371
321
 
372
- # Callback called when a job is triggered. If the lock cannot be acquired,
373
- # the job won't run (though it'll still be scheduled to run again if
374
- # necessary).
375
- #
376
- def confirm_lock
322
+ @jobs[job_id]
323
+ end
377
324
 
378
- @trigger_lock.lock
379
- end
325
+ # Returns true if the scheduler has acquired the [exclusive] lock and
326
+ # thus may run.
327
+ #
328
+ # Most of the time, a scheduler is run alone and this method should
329
+ # return true. It is useful in cases where among a group of applications
330
+ # only one of them should run the scheduler. For schedulers that should
331
+ # not run, the method should return false.
332
+ #
333
+ # Out of the box, rufus-scheduler proposes the
334
+ # :lockfile => 'path/to/lock/file' scheduler start option. It makes
335
+ # it easy for schedulers on the same machine to determine which should
336
+ # run (the first to write the lockfile and lock it). It uses "man 2 flock"
337
+ # so it probably won't work reliably on distributed file systems.
338
+ #
339
+ # If one needs to use a special/different locking mechanism, the scheduler
340
+ # accepts :scheduler_lock => lock_object. lock_object only needs to respond
341
+ # to #lock
342
+ # and #unlock, and both of these methods should be idempotent.
343
+ #
344
+ # Look at rufus/scheduler/locks.rb for an example.
345
+ #
346
+ def lock
347
+
348
+ @scheduler_lock.lock
349
+ end
380
350
 
381
- # Returns true if this job is currently scheduled.
382
- #
383
- # Takes extra care to answer true if the job is a repeat job
384
- # currently firing.
385
- #
386
- def scheduled?(job_or_job_id)
351
+ # Sister method to #lock, is called when the scheduler shuts down.
352
+ #
353
+ def unlock
387
354
 
388
- job, job_id = fetch(job_or_job_id)
355
+ @trigger_lock.unlock
356
+ @scheduler_lock.unlock
357
+ end
389
358
 
390
- !! (job && job.next_time != nil)
391
- end
359
+ # Callback called when a job is triggered. If the lock cannot be acquired,
360
+ # the job won't run (though it'll still be scheduled to run again if
361
+ # necessary).
362
+ #
363
+ def confirm_lock
392
364
 
393
- # Lists all the threads associated with this scheduler.
394
- #
395
- def threads
365
+ @trigger_lock.lock
366
+ end
396
367
 
397
- Thread.list.select { |t| t[thread_key] }
398
- end
368
+ # Returns true if this job is currently scheduled.
369
+ #
370
+ # Takes extra care to answer true if the job is a repeat job
371
+ # currently firing.
372
+ #
373
+ def scheduled?(job_or_job_id)
399
374
 
400
- # Lists all the work threads (the ones actually running the scheduled
401
- # block code)
402
- #
403
- # Accepts a query option, which can be set to:
404
- # * :all (default), returns all the threads that are work threads
405
- # or are currently running a job
406
- # * :active, returns all threads that are currenly running a job
407
- # * :vacant, returns the threads that are not running a job
408
- #
409
- # If, thanks to :blocking => true, a job is scheduled to monopolize the
410
- # main scheduler thread, that thread will get returned when :active or
411
- # :all.
412
- #
413
- def work_threads(query=:all)
414
-
415
- ts =
416
- threads.select { |t|
417
- t[:rufus_scheduler_job] || t[:rufus_scheduler_work_thread]
418
- }
419
-
420
- case query
421
- when :active then ts.select { |t| t[:rufus_scheduler_job] }
422
- when :vacant then ts.reject { |t| t[:rufus_scheduler_job] }
423
- else ts
424
- end
425
- end
375
+ job, _ = fetch(job_or_job_id)
426
376
 
427
- def running_jobs(opts={})
377
+ !! (job && job.unscheduled_at.nil? && job.next_time != nil)
378
+ end
379
+
380
+ # Lists all the threads associated with this scheduler.
381
+ #
382
+ def threads
428
383
 
429
- jobs(opts.merge(:running => true))
384
+ Thread.list.select { |t| t[thread_key] }
385
+ end
386
+
387
+ # Lists all the work threads (the ones actually running the scheduled
388
+ # block code)
389
+ #
390
+ # Accepts a query option, which can be set to:
391
+ # * :all (default), returns all the threads that are work threads
392
+ # or are currently running a job
393
+ # * :active, returns all threads that are currently running a job
394
+ # * :vacant, returns the threads that are not running a job
395
+ #
396
+ # If, thanks to :blocking => true, a job is scheduled to monopolize the
397
+ # main scheduler thread, that thread will get returned when :active or
398
+ # :all.
399
+ #
400
+ def work_threads(query=:all)
401
+
402
+ ts = threads.select { |t| t[:rufus_scheduler_work_thread] }
403
+
404
+ case query
405
+ when :active then ts.select { |t| t[:rufus_scheduler_job] }
406
+ when :vacant then ts.reject { |t| t[:rufus_scheduler_job] }
407
+ else ts
430
408
  end
409
+ end
431
410
 
432
- def occurrences(time0, time1, format=:per_job)
411
+ def running_jobs(opts={})
433
412
 
434
- h = {}
413
+ jobs(opts.merge(:running => true))
414
+ end
435
415
 
436
- jobs.each do |j|
437
- os = j.occurrences(time0, time1)
438
- h[j] = os if os.any?
439
- end
416
+ def occurrences(time0, time1, format=:per_job)
440
417
 
441
- if format == :timeline
442
- a = []
443
- h.each { |j, ts| ts.each { |t| a << [ t, j ] } }
444
- a.sort_by { |(t, j)| t }
445
- else
446
- h
447
- end
448
- end
418
+ h = {}
449
419
 
450
- def timeline(time0, time1)
420
+ jobs.each do |j|
421
+ os = j.occurrences(time0, time1)
422
+ h[j] = os if os.any?
423
+ end
451
424
 
452
- occurrences(time0, time1, :timeline)
425
+ if format == :timeline
426
+ a = []
427
+ h.each { |j, ts| ts.each { |t| a << [ t, j ] } }
428
+ a.sort_by { |(t, _)| t }
429
+ else
430
+ h
453
431
  end
432
+ end
454
433
 
455
- def on_error(job, err)
434
+ def timeline(time0, time1)
435
+
436
+ occurrences(time0, time1, :timeline)
437
+ end
456
438
 
457
- pre = err.object_id.to_s
439
+ def on_error(job, err)
458
440
 
459
- ms = {}; mutexes.each { |k, v| ms[k] = v.locked? }
441
+ pre = err.object_id.to_s
460
442
 
461
- stderr.puts("{ #{pre} rufus-scheduler intercepted an error:")
443
+ ms = {}; mutexes.each { |k, v| ms[k] = v.locked? }
444
+
445
+ stderr.puts("{ #{pre} rufus-scheduler intercepted an error:")
446
+ if job
462
447
  stderr.puts(" #{pre} job:")
463
448
  stderr.puts(" #{pre} #{job.class} #{job.original.inspect} #{job.opts.inspect}")
449
+ stderr.puts(" #{pre} #{job.source_location.inspect}")
464
450
  # TODO: eventually use a Job#detail or something like that
465
- stderr.puts(" #{pre} error:")
466
- stderr.puts(" #{pre} #{err.object_id}")
467
- stderr.puts(" #{pre} #{err.class}")
468
- stderr.puts(" #{pre} #{err}")
469
- err.backtrace.each do |l|
470
- stderr.puts(" #{pre} #{l}")
451
+ else
452
+ stderr.puts(" #{pre} job: (error did not occur in a job)")
453
+ end
454
+ stderr.puts(" #{pre} error:")
455
+ stderr.puts(" #{pre} #{err.object_id}")
456
+ stderr.puts(" #{pre} #{err.class}")
457
+ stderr.puts(" #{pre} #{err}")
458
+ err.backtrace.each do |l|
459
+ stderr.puts(" #{pre} #{l}")
460
+ end
461
+ stderr.puts(" #{pre} tz:")
462
+ stderr.puts(" #{pre} ENV['TZ']: #{ENV['TZ']}")
463
+ stderr.puts(" #{pre} Time.now: #{Time.now}")
464
+ stderr.puts(" #{pre} local_tzone: #{EoTime.local_tzone.inspect}")
465
+ stderr.puts(" #{pre} et-orbi:")
466
+ stderr.puts(" #{pre} #{EoTime.platform_info}")
467
+ stderr.puts(" #{pre} scheduler:")
468
+ stderr.puts(" #{pre} object_id: #{object_id}")
469
+ stderr.puts(" #{pre} opts:")
470
+ stderr.puts(" #{pre} #{@opts.inspect}")
471
+ stderr.puts(" #{pre} frequency: #{self.frequency}")
472
+ stderr.puts(" #{pre} scheduler_lock: #{@scheduler_lock.inspect}")
473
+ stderr.puts(" #{pre} trigger_lock: #{@trigger_lock.inspect}")
474
+ stderr.puts(" #{pre} uptime: #{uptime} (#{uptime_s})")
475
+ stderr.puts(" #{pre} down?: #{down?}")
476
+ stderr.puts(" #{pre} frequency: #{frequency.inspect}")
477
+ stderr.puts(" #{pre} discard_past: #{discard_past.inspect}")
478
+ stderr.puts(" #{pre} started_at: #{started_at.inspect}")
479
+ stderr.puts(" #{pre} paused_at: #{paused_at.inspect}")
480
+ stderr.puts(" #{pre} threads: #{self.threads.size}")
481
+ stderr.puts(" #{pre} thread: #{self.thread}")
482
+ stderr.puts(" #{pre} thread_key: #{self.thread_key}")
483
+ stderr.puts(" #{pre} work_threads: #{work_threads.size}")
484
+ stderr.puts(" #{pre} active: #{work_threads(:active).size}")
485
+ stderr.puts(" #{pre} vacant: #{work_threads(:vacant).size}")
486
+ stderr.puts(" #{pre} max_work_threads: #{max_work_threads}")
487
+ stderr.puts(" #{pre} mutexes: #{ms.inspect}")
488
+ stderr.puts(" #{pre} jobs: #{jobs.size}")
489
+ stderr.puts(" #{pre} at_jobs: #{at_jobs.size}")
490
+ stderr.puts(" #{pre} in_jobs: #{in_jobs.size}")
491
+ stderr.puts(" #{pre} every_jobs: #{every_jobs.size}")
492
+ stderr.puts(" #{pre} interval_jobs: #{interval_jobs.size}")
493
+ stderr.puts(" #{pre} cron_jobs: #{cron_jobs.size}")
494
+ stderr.puts(" #{pre} running_jobs: #{running_jobs.size}")
495
+ stderr.puts(" #{pre} work_queue:")
496
+ stderr.puts(" #{pre} size: #{@work_queue.size}")
497
+ stderr.puts(" #{pre} num_waiting: #{@work_queue.num_waiting}")
498
+ stderr.puts(" #{pre} join_queue:")
499
+ stderr.puts(" #{pre} size: #{@join_queue.size}")
500
+ stderr.puts(" #{pre} num_waiting: #{@join_queue.num_waiting}")
501
+ stderr.puts("} #{pre} .")
502
+
503
+ rescue => e
504
+
505
+ stderr.puts("failure in #on_error itself:")
506
+ stderr.puts(e.inspect)
507
+ stderr.puts(e.backtrace)
508
+
509
+ ensure
510
+
511
+ stderr.flush
512
+ end
513
+
514
+ def shutdown(opt=nil)
515
+
516
+ opts =
517
+ case opt
518
+ when Symbol then { opt => true }
519
+ when Hash then opt
520
+ else {}
471
521
  end
472
- stderr.puts(" #{pre} tz:")
473
- stderr.puts(" #{pre} ENV['TZ']: #{ENV['TZ']}")
474
- stderr.puts(" #{pre} Time.now: #{Time.now}")
475
- stderr.puts(" #{pre} scheduler:")
476
- stderr.puts(" #{pre} object_id: #{object_id}")
477
- stderr.puts(" #{pre} opts:")
478
- stderr.puts(" #{pre} #{@opts.inspect}")
479
- stderr.puts(" #{pre} frequency: #{self.frequency}")
480
- stderr.puts(" #{pre} scheduler_lock: #{@scheduler_lock.inspect}")
481
- stderr.puts(" #{pre} trigger_lock: #{@trigger_lock.inspect}")
482
- stderr.puts(" #{pre} uptime: #{uptime} (#{uptime_s})")
483
- stderr.puts(" #{pre} down?: #{down?}")
484
- stderr.puts(" #{pre} threads: #{self.threads.size}")
485
- stderr.puts(" #{pre} thread: #{self.thread}")
486
- stderr.puts(" #{pre} thread_key: #{self.thread_key}")
487
- stderr.puts(" #{pre} work_threads: #{work_threads.size}")
488
- stderr.puts(" #{pre} active: #{work_threads(:active).size}")
489
- stderr.puts(" #{pre} vacant: #{work_threads(:vacant).size}")
490
- stderr.puts(" #{pre} max_work_threads: #{max_work_threads}")
491
- stderr.puts(" #{pre} mutexes: #{ms.inspect}")
492
- stderr.puts(" #{pre} jobs: #{jobs.size}")
493
- stderr.puts(" #{pre} at_jobs: #{at_jobs.size}")
494
- stderr.puts(" #{pre} in_jobs: #{in_jobs.size}")
495
- stderr.puts(" #{pre} every_jobs: #{every_jobs.size}")
496
- stderr.puts(" #{pre} interval_jobs: #{interval_jobs.size}")
497
- stderr.puts(" #{pre} cron_jobs: #{cron_jobs.size}")
498
- stderr.puts(" #{pre} running_jobs: #{running_jobs.size}")
499
- stderr.puts(" #{pre} work_queue: #{work_queue.size}")
500
- stderr.puts("} #{pre} .")
501
-
502
- rescue => e
503
-
504
- stderr.puts("failure in #on_error itself:")
505
- stderr.puts(e.inspect)
506
- stderr.puts(e.backtrace)
507
-
508
- ensure
509
-
510
- stderr.flush
522
+
523
+ @jobs.unschedule_all
524
+
525
+ if opts[:wait] || opts[:join]
526
+ join_shutdown(opts)
527
+ elsif opts[:kill]
528
+ kill_shutdown(opts)
529
+ else
530
+ regular_shutdown(opts)
511
531
  end
512
532
 
513
- protected
533
+ @work_queue.clear
514
534
 
515
- # Returns [ job, job_id ]
516
- #
517
- def fetch(job_or_job_id)
535
+ unlock
518
536
 
519
- if job_or_job_id.respond_to?(:job_id)
520
- [ job_or_job_id, job_or_job_id.job_id ]
521
- else
522
- [ job(job_or_job_id), job_or_job_id ]
523
- end
524
- end
537
+ @thread.join unless @thread == Thread.current
538
+ end
539
+ alias stop shutdown
525
540
 
526
- def terminate_all_jobs
541
+ protected
527
542
 
528
- jobs.each { |j| j.unschedule }
543
+ def join_shutdown(opts)
529
544
 
530
- sleep 0.01 while running_jobs.size > 0
531
- end
545
+ limit = opts[:wait] || opts[:join]
546
+ limit = limit.is_a?(Numeric) ? limit : nil
547
+
548
+ #@started_at = nil
549
+ #
550
+ # when @started_at is nil, the scheduler thread exits, here
551
+ # we want it to exit when all the work threads have been joined
552
+ # hence it's set to nil later on
553
+ #
554
+ @paused_at = EoTime.now
532
555
 
533
- def join_all_work_threads
556
+ (work_threads.size * 2 + 1).times { @work_queue << :shutdown }
534
557
 
535
- work_threads.size.times { @work_queue << :sayonara }
558
+ work_threads
559
+ .collect { |wt|
560
+ wt == Thread.current ? nil : Thread.new { wt.join(limit); wt.kill } }
561
+ .each { |st|
562
+ st.join if st }
563
+
564
+ @started_at = nil
565
+ end
536
566
 
537
- work_threads.each { |t| t.join }
567
+ def kill_shutdown(opts)
538
568
 
539
- @work_queue.clear
569
+ @started_at = nil
570
+ work_threads.each(&:kill)
571
+ end
572
+
573
+ def regular_shutdown(opts)
574
+
575
+ @started_at = nil
576
+ end
577
+
578
+ def time_limit_join(limit)
579
+
580
+ fail ArgumentError.new("limit #{limit.inspect} should be > 0") \
581
+ unless limit.is_a?(Numeric) && limit > 0
582
+
583
+ t0 = monow
584
+ f = [ limit.to_f / 20, 0.100 ].min
585
+
586
+ while monow - t0 < limit
587
+ r =
588
+ begin
589
+ @join_queue.pop(true)
590
+ rescue ThreadError
591
+ # #<ThreadError: queue empty>
592
+ false
593
+ end
594
+ return r if r
595
+ sleep(f)
540
596
  end
541
597
 
542
- def kill_all_work_threads
598
+ nil
599
+ end
600
+
601
+ def no_time_limit_join
602
+
603
+ @join_queue.pop
604
+ end
605
+
606
+ # Returns [ job, job_id ]
607
+ #
608
+ def fetch(job_or_job_id)
543
609
 
544
- work_threads.each { |t| t.kill }
610
+ if job_or_job_id.respond_to?(:job_id)
611
+ [ job_or_job_id, job_or_job_id.job_id ]
612
+ else
613
+ [ job(job_or_job_id), job_or_job_id ]
545
614
  end
615
+ end
616
+
617
+ def terminate_all_jobs
546
618
 
547
- #def free_all_work_threads
548
- #
549
- # work_threads.each { |t| t.raise(KillSignal) }
550
- #end
619
+ jobs.each { |j| j.unschedule }
620
+
621
+ sleep 0.01 while running_jobs.size > 0
622
+ end
551
623
 
552
- def start
624
+ #def free_all_work_threads
625
+ #
626
+ # work_threads.each { |t| t.raise(KillSignal) }
627
+ #end
553
628
 
554
- @started_at = Time.now
629
+ def start
555
630
 
556
- @thread =
557
- Thread.new do
631
+ @started_at = EoTime.now
558
632
 
559
- while @started_at do
633
+ @thread =
634
+ Thread.new do
635
+
636
+ while @started_at do
637
+ begin
560
638
 
561
639
  unschedule_jobs
562
- trigger_jobs unless @paused
640
+ trigger_jobs unless @paused_at
563
641
  timeout_jobs
564
642
 
565
643
  sleep(@frequency)
644
+
645
+ rescue => err
646
+ #
647
+ # for `blocking: true` jobs mostly
648
+ #
649
+ on_error(nil, err)
566
650
  end
567
651
  end
568
652
 
569
- @thread[@thread_key] = true
570
- @thread[:rufus_scheduler] = self
571
- @thread[:name] = @opts[:thread_name] || "#{@thread_key}_scheduler"
572
- end
653
+ rejoin
654
+ end
573
655
 
574
- def unschedule_jobs
656
+ @thread[@thread_key] = true
657
+ @thread[:rufus_scheduler] = self
658
+ @thread[:name] = @opts[:thread_name] || "#{@thread_key}_scheduler"
659
+ end
575
660
 
576
- @jobs.delete_unscheduled
577
- end
661
+ def unschedule_jobs
578
662
 
579
- def trigger_jobs
663
+ @jobs.delete_unscheduled
664
+ end
580
665
 
581
- now = Time.now
666
+ def trigger_jobs
582
667
 
583
- @jobs.each(now) do |job|
668
+ now = EoTime.now
584
669
 
585
- job.trigger(now)
586
- end
670
+ @jobs.each(now) do |job|
671
+
672
+ job.trigger(now)
587
673
  end
674
+ end
588
675
 
589
- def timeout_jobs
676
+ def timeout_jobs
590
677
 
591
- work_threads(:active).each do |t|
678
+ work_threads(:active).each do |t|
592
679
 
593
- job = t[:rufus_scheduler_job]
594
- to = t[:rufus_scheduler_timeout]
595
- ts = t[:rufus_scheduler_time]
680
+ job = t[:rufus_scheduler_job]
681
+ to = t[:rufus_scheduler_timeout]
682
+ ts = t[:rufus_scheduler_time]
596
683
 
597
- next unless job && to && ts
598
- # thread might just have become inactive (job -> nil)
684
+ next unless job && to && ts
685
+ # thread might just have become inactive (job -> nil)
599
686
 
600
- to = to.is_a?(Time) ? to : ts + to
687
+ to = ts + to unless to.is_a?(EoTime)
601
688
 
602
- next if to > Time.now
689
+ next if to > EoTime.now
603
690
 
604
- t.raise(Rufus::Scheduler::TimeoutError)
605
- end
691
+ t.raise(Rufus::Scheduler::TimeoutError)
606
692
  end
693
+ end
607
694
 
608
- def do_schedule(job_type, t, callable, opts, return_job_instance, block)
609
-
610
- fail NotRunningError.new(
611
- 'cannot schedule, scheduler is down or shutting down'
612
- ) if @started_at == nil
613
-
614
- callable, opts = nil, callable if callable.is_a?(Hash)
615
- return_job_instance ||= opts[:job]
616
-
617
- job_class =
618
- case job_type
619
- when :once
620
- opts[:_t] ||= Rufus::Scheduler.parse(t, opts)
621
- opts[:_t].is_a?(Time) ? AtJob : InJob
622
- when :every
623
- EveryJob
624
- when :interval
625
- IntervalJob
626
- when :cron
627
- CronJob
628
- end
695
+ def rejoin
629
696
 
630
- job = job_class.new(self, t, opts, block || callable)
697
+ (@join_queue.num_waiting * 2 + 1).times { @join_queue << @thread }
698
+ end
631
699
 
632
- raise ArgumentError.new(
633
- "job frequency (#{job.frequency}) is higher than " +
634
- "scheduler frequency (#{@frequency})"
635
- ) if job.respond_to?(:frequency) && job.frequency < @frequency
700
+ def do_schedule(job_type, t, callable, opts, return_job_instance, block)
636
701
 
637
- @jobs.push(job)
702
+ fail NotRunningError.new(
703
+ 'cannot schedule, scheduler is down or shutting down'
704
+ ) if @started_at.nil?
638
705
 
639
- return_job_instance ? job : job.job_id
640
- end
706
+ callable, opts = nil, callable if callable.is_a?(Hash)
707
+ opts = opts.dup unless opts.has_key?(:_t)
708
+
709
+ return_job_instance ||= opts[:job]
710
+
711
+ job_class =
712
+ case job_type
713
+ when :once
714
+ opts[:_t] ||= Rufus::Scheduler.parse(t, opts)
715
+ opts[:_t].is_a?(Numeric) ? InJob : AtJob
716
+ when :every
717
+ EveryJob
718
+ when :interval
719
+ IntervalJob
720
+ when :cron
721
+ CronJob
722
+ end
723
+
724
+ job = job_class.new(self, t, opts, block || callable)
725
+ job.check_frequency
726
+
727
+ @jobs.push(job)
728
+
729
+ return_job_instance ? job : job.job_id
641
730
  end
731
+
732
+ def monow; self.class.monow; end
733
+ def ltstamp; self.class.ltstamp; end
642
734
  end
643
735