rufus-scheduler 3.5.2 → 3.8.1

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