rufus-scheduler 3.5.1 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,655 +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.1'
12
+ VERSION = '3.8.0'
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) } }
271
291
 
272
- # Returns all the scheduled jobs
273
- # (even those right before re-schedule).
274
- #
275
- def jobs(opts={})
292
+ jobs
293
+ end
276
294
 
277
- opts = { opts => true } if opts.is_a?(Symbol)
295
+ def at_jobs(opts={})
278
296
 
279
- jobs = @jobs.to_a
297
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::AtJob) }
298
+ end
280
299
 
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
300
+ def in_jobs(opts={})
286
301
 
287
- tags = Array(opts[:tag] || opts[:tags]).collect(&:to_s)
288
- jobs = jobs.reject { |j| tags.find { |t| ! j.tags.include?(t) } }
302
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::InJob) }
303
+ end
289
304
 
290
- jobs
291
- end
305
+ def every_jobs(opts={})
292
306
 
293
- def at_jobs(opts={})
307
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::EveryJob) }
308
+ end
294
309
 
295
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::AtJob) }
296
- end
310
+ def interval_jobs(opts={})
297
311
 
298
- def in_jobs(opts={})
312
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::IntervalJob) }
313
+ end
299
314
 
300
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::InJob) }
301
- end
315
+ def cron_jobs(opts={})
302
316
 
303
- def every_jobs(opts={})
317
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::CronJob) }
318
+ end
304
319
 
305
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::EveryJob) }
306
- end
320
+ def job(job_id)
307
321
 
308
- def interval_jobs(opts={})
322
+ @jobs[job_id]
323
+ end
309
324
 
310
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::IntervalJob) }
311
- 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
312
350
 
313
- def cron_jobs(opts={})
351
+ # Sister method to #lock, is called when the scheduler shuts down.
352
+ #
353
+ def unlock
314
354
 
315
- jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::CronJob) }
316
- end
355
+ @trigger_lock.unlock
356
+ @scheduler_lock.unlock
357
+ end
317
358
 
318
- def job(job_id)
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
319
364
 
320
- @jobs[job_id]
321
- end
365
+ @trigger_lock.lock
366
+ end
322
367
 
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
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)
348
374
 
349
- # Sister method to #lock, is called when the scheduler shuts down.
350
- #
351
- def unlock
375
+ job, _ = fetch(job_or_job_id)
352
376
 
353
- @trigger_lock.unlock
354
- @scheduler_lock.unlock
355
- end
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
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 }
559
+
560
+ @started_at = nil
561
+ end
562
+
563
+ def kill_shutdown(opts)
510
564
 
511
- def terminate_all_jobs
565
+ @started_at = nil
566
+ work_threads.each(&:kill)
567
+ end
512
568
 
513
- jobs.each { |j| j.unschedule }
569
+ def regular_shutdown(opts)
514
570
 
515
- sleep 0.01 while running_jobs.size > 0
516
- end
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 => e
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
528
598
 
529
- work_threads.each { |t| t.kill }
599
+ @join_queue.pop
600
+ end
601
+
602
+ # Returns [ job, job_id ]
603
+ #
604
+ def fetch(job_or_job_id)
605
+
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
614
+
615
+ jobs.each { |j| j.unschedule }
616
+
617
+ sleep 0.01 while running_jobs.size > 0
618
+ end
531
619
 
532
- #def free_all_work_threads
533
- #
534
- # work_threads.each { |t| t.raise(KillSignal) }
535
- #end
620
+ #def free_all_work_threads
621
+ #
622
+ # work_threads.each { |t| t.raise(KillSignal) }
623
+ #end
536
624
 
537
- def start
625
+ def start
538
626
 
539
- @started_at = EoTime.now
627
+ @started_at = EoTime.now
540
628
 
541
- @thread =
542
- Thread.new do
629
+ @thread =
630
+ Thread.new do
543
631
 
544
- while @started_at do
632
+ while @started_at do
545
633
 
546
- unschedule_jobs
547
- trigger_jobs unless @paused
548
- timeout_jobs
634
+ unschedule_jobs
635
+ trigger_jobs unless @paused_at
636
+ timeout_jobs
549
637
 
550
- sleep(@frequency)
551
- end
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
558
643
 
559
- def unschedule_jobs
644
+ @thread[@thread_key] = true
645
+ @thread[:rufus_scheduler] = self
646
+ @thread[:name] = @opts[:thread_name] || "#{@thread_key}_scheduler"
647
+ end
560
648
 
561
- @jobs.delete_unscheduled
562
- end
649
+ def unschedule_jobs
563
650
 
564
- def trigger_jobs
651
+ @jobs.delete_unscheduled
652
+ end
565
653
 
566
- now = EoTime.now
654
+ def trigger_jobs
567
655
 
568
- @jobs.each(now) do |job|
656
+ now = EoTime.now
569
657
 
570
- job.trigger(now)
571
- end
658
+ @jobs.each(now) do |job|
659
+
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
592
682
 
593
- def do_schedule(job_type, t, callable, opts, return_job_instance, block)
683
+ def rejoin
594
684
 
595
- fail NotRunningError.new(
596
- 'cannot schedule, scheduler is down or shutting down'
597
- ) if @started_at.nil?
685
+ (@join_queue.num_waiting * 2 + 1).times { @join_queue << @thread }
686
+ end
598
687
 
599
- callable, opts = nil, callable if callable.is_a?(Hash)
600
- opts = opts.dup unless opts.has_key?(:_t)
688
+ def do_schedule(job_type, t, callable, opts, return_job_instance, block)
601
689
 
602
- return_job_instance ||= opts[:job]
690
+ fail NotRunningError.new(
691
+ 'cannot schedule, scheduler is down or shutting down'
692
+ ) if @started_at.nil?
603
693
 
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
694
+ callable, opts = nil, callable if callable.is_a?(Hash)
695
+ opts = opts.dup unless opts.has_key?(:_t)
616
696
 
617
- job = job_class.new(self, t, opts, block || callable)
618
-
619
- #fail ArgumentError.new(
620
- # "job frequency (#{job.frequency}) is higher than " +
621
- # "scheduler frequency (#{@frequency})"
622
- #) if job.respond_to?(:frequency) && job.frequency < @frequency
623
- #
624
- # This was expensive
625
-
626
- if (
627
- ! job.is_a?(Rufus::Scheduler::IntervalJob) &&
628
- job.methods.include?(:next_time_from)
629
- ) then
630
-
631
- nts = (1..365)
632
- .inject([ job.send(:next_time_from, EtOrbi.now) ]) { |a, i|
633
- a << job.send(:next_time_from, a.last); a }
634
- deltas = []; prev = nts.shift
635
- while (nt = nts.shift); deltas << (nt - prev).to_f; prev = nt; end
636
-
637
- deltas = deltas[1..-1] \
638
- if opts.keys.find { |k| k.to_s.match(/\Afirst/) }
639
- #
640
- # do not consider the first delta if there is a first, first_at,
641
- # or first_in involved
642
-
643
- fail ArgumentError.new(
644
- "job frequency (~max #{deltas.min}s) is higher than " +
645
- "scheduler frequency (#{@frequency})"
646
- ) if deltas.min < @frequency * 0.9
697
+ return_job_instance ||= opts[:job]
698
+
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
647
710
  end
648
711
 
649
- @jobs.push(job)
712
+ job = job_class.new(self, t, opts, block || callable)
713
+ job.check_frequency
650
714
 
651
- return_job_instance ? job : job.job_id
652
- end
715
+ @jobs.push(job)
716
+
717
+ return_job_instance ? job : job.job_id
653
718
  end
719
+
720
+ def monow; self.class.monow; end
721
+ def ltstamp; self.class.ltstamp; end
654
722
  end
655
723