rufus-scheduler 2.0.24 → 3.0.0

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.
Files changed (53) hide show
  1. data/CHANGELOG.txt +6 -0
  2. data/CREDITS.txt +4 -0
  3. data/README.md +1064 -0
  4. data/Rakefile +1 -4
  5. data/TODO.txt +145 -55
  6. data/lib/rufus/scheduler.rb +502 -26
  7. data/lib/rufus/{sc → scheduler}/cronline.rb +46 -17
  8. data/lib/rufus/{sc/version.rb → scheduler/job_array.rb} +56 -4
  9. data/lib/rufus/scheduler/jobs.rb +548 -0
  10. data/lib/rufus/scheduler/util.rb +318 -0
  11. data/rufus-scheduler.gemspec +30 -4
  12. data/spec/cronline_spec.rb +29 -8
  13. data/spec/error_spec.rb +116 -0
  14. data/spec/job_array_spec.rb +39 -0
  15. data/spec/job_at_spec.rb +58 -0
  16. data/spec/job_cron_spec.rb +67 -0
  17. data/spec/job_every_spec.rb +71 -0
  18. data/spec/job_in_spec.rb +20 -0
  19. data/spec/job_interval_spec.rb +68 -0
  20. data/spec/job_repeat_spec.rb +308 -0
  21. data/spec/job_spec.rb +387 -115
  22. data/spec/lockfile_spec.rb +61 -0
  23. data/spec/parse_spec.rb +203 -0
  24. data/spec/schedule_at_spec.rb +129 -0
  25. data/spec/schedule_cron_spec.rb +66 -0
  26. data/spec/schedule_every_spec.rb +109 -0
  27. data/spec/schedule_in_spec.rb +80 -0
  28. data/spec/schedule_interval_spec.rb +128 -0
  29. data/spec/scheduler_spec.rb +831 -124
  30. data/spec/spec_helper.rb +65 -0
  31. data/spec/threads_spec.rb +75 -0
  32. metadata +64 -59
  33. data/README.rdoc +0 -661
  34. data/lib/rufus/otime.rb +0 -3
  35. data/lib/rufus/sc/jobqueues.rb +0 -160
  36. data/lib/rufus/sc/jobs.rb +0 -471
  37. data/lib/rufus/sc/rtime.rb +0 -363
  38. data/lib/rufus/sc/scheduler.rb +0 -636
  39. data/spec/at_in_spec.rb +0 -47
  40. data/spec/at_spec.rb +0 -125
  41. data/spec/blocking_spec.rb +0 -64
  42. data/spec/cron_spec.rb +0 -134
  43. data/spec/every_spec.rb +0 -304
  44. data/spec/exception_spec.rb +0 -113
  45. data/spec/in_spec.rb +0 -150
  46. data/spec/mutex_spec.rb +0 -159
  47. data/spec/rtime_spec.rb +0 -137
  48. data/spec/schedulable_spec.rb +0 -97
  49. data/spec/spec_base.rb +0 -87
  50. data/spec/stress_schedule_unschedule_spec.rb +0 -159
  51. data/spec/timeout_spec.rb +0 -148
  52. data/test/kjw.rb +0 -113
  53. data/test/t.rb +0 -20
data/Rakefile CHANGED
@@ -1,8 +1,5 @@
1
1
 
2
- $:.unshift('.') # 1.9.2
3
-
4
2
  require 'rubygems'
5
- require 'rubygems/user_interaction' if Gem::RubyGemsVersion == '1.5.0'
6
3
 
7
4
  require 'rake'
8
5
  require 'rake/clean'
@@ -42,7 +39,7 @@ desc %{
42
39
  task :build do
43
40
 
44
41
  sh "gem build #{GEMSPEC_FILE}"
45
- sh "mkdir pkg" rescue nil
42
+ sh "mkdir -p pkg"
46
43
  sh "mv #{GEMSPEC.name}-#{GEMSPEC.version}.gem pkg/"
47
44
  end
48
45
 
data/TODO.txt CHANGED
@@ -1,57 +1,147 @@
1
1
 
2
- [o] spec for jobs in the past (in and at)
3
- [o] :discard_past
4
-
5
- [o] every
6
- [o] cron
7
-
8
- [o] CHECK every and unschedule !!!
9
-
10
- [o] :tags
11
- [o] timeout feature (at/in/every/cron) in Job class
12
-
13
- [o] :first_in, :first_at
14
-
15
- [x] :dont_reschedule (or block returns false ?)
16
-
17
- [o] [get_]jobs methods
18
- [o] find methods
19
-
20
- [x] CTRL-C during tests : allow, trap_int...
21
-
22
- [o] 1.9
23
- [o] j1.2.0
24
-
25
- [o] revise trigger block arity
26
- use a compatibility switch ? yes
27
-
28
- [o] synchronize @cron_jobs ?
29
-
30
- [o] why not : make it work even if EM is not present
31
- EmScheduler < Scheduler
32
- FiberScheduler < Scheduler
33
-
34
- [x] :blocking => 'blockname' idea, mutex = @mutexes['blockname'] ...
35
- [o] eventually, make sleep frequency customizable
36
-
37
- [o] PlainScheduler : name thread
38
-
39
- [o] document :blocking
40
-
41
- [o] README.rdoc
42
- [o] fix jruby120 --em
43
-
44
- [o] handle_exception (job, e)
45
-
46
- [o] Schedulable
47
-
48
- [o] Rufus::Scheduler.start_new() : autodetect EM ?
49
- [o] check :blocking and every (reschedule blocking...)
50
- [o] document :thread_name scheduler option
51
-
52
- [o] unify cron_jobs#trigger_matching_jobs(now) and jobs#job_to_trigger
53
- [o] pluggable job queues
54
-
55
- [ ] Joel's complaint about timeout jobs gone ballistic
56
- [x] move trigger_job out of the scheduler
2
+ [o] merge schedule_queue and unschedule_queue (and merge [un]schedule steps)
3
+ [x] OR stop using queue, since we've got the thread-safe JobArray
4
+ [x] if possible, drop the mutex in JobArray
5
+ NO, that mutex is necessary for Scheduler#jobs (on JRuby an co)...
6
+ [o] named mutexes
7
+ [o] drop the schedule queue, rely on the mutex in JobArray
8
+ [o] def jobs; (@jobs.to_a + running_jobs).uniq; end
9
+ [o] replace @unscheduled by @unscheduled_at
10
+ [o] make sure #jobs doesn't return unscheduled jobs
11
+ [o] job tags and find_by_tag(t) (as in rs 2.x)
12
+ [o] require tzinfo anyway (runtime dep)
13
+ [o] document frequency
14
+ [o] accept :frequency => '5s'
15
+ [o] timeout (as in rufus-scheduler 2.x)
16
+ [o] Rufus::Scheduler#running_jobs (as in rufus-scheduler 2.x)
17
+ [o] Rufus::Scheduler#terminate_all_jobs
18
+ [o] Rufus::Scheduler::Job#kill
19
+ [x] Rufus::Scheduler#kill_all_jobs
20
+ [o] Rufus::Scheduler#shutdown(:terminate or :kill (or nothing))
21
+ [o] RepeatJob #pause / #resume (think about discard past)
22
+ [o] Rufus::Scheduler.start_new (backward comp) (with deprec note?)
23
+ [o] pass job to scheduled block? What does rs 2.x do?
24
+ [o] :first[_in|_at] for RepeatJob
25
+ [o] :last[_in|_at] for RepeatJob
26
+ [o] :times for RepeatJob (how many recurrences)
27
+ [o] fix issue #39 (first_at parses as UTC)
28
+ [o] about issue #43, raise if cron/every job frequency < scheduler frequency
29
+ [o] unlock spec/parse_spec.rb:30 "parse datimes with timezones"
30
+ [o] some kind of Schedulable (Xyz#call(job, time))
31
+ [o] add Jruby and Rubinius to Travis
32
+ [o] make Job #first_at= / #last_at= automatically parse strings?
33
+ [o] bring in Kratob's spec about mutex vs timeout and adapt 3.0 to it,
34
+ https://github.com/jmettraux/rufus-scheduler/pull/67
35
+ [x] :unschedule_if => lambda { |job| ... }
36
+ [o] OR look at how it was done in rs 2.0.x, some return value?
37
+ no, pass the job as arg to the block, then let the block do job.unschedule
38
+ so, document schedule.every('10d') { |j| j.unschedule if x?() }
39
+ [x] remove the time in job.trigger(time)
40
+ [o] add spec for job queued then unscheduled
41
+ [o] add spec for Scheduler#shutdown and work threads
42
+ [o] at some point, bring back rbx19 to Travis
43
+ [o] move the parse/util part of scheduler.rb to util.rb
44
+ [o] rescue KillSignal in job thread loop to kill just the job
45
+ [o] add spec for raise if scheduling a job while scheduler is shutting down
46
+ [o] schedule_in(2.days.from_now) {}
47
+ at and in could understand each others time parameter, ftw...
48
+ use the new #parse_to_time? no
49
+ [o] do repeat jobs reschedule after timing out? yes
50
+ [o] schedule_interval('20s')?
51
+ [x] Scheduler#reschedule(job) (new copy of the job)
52
+ [x] #free_all_work_threads is missing an implementation
53
+ [x] rescue StandardError
54
+ :on_error => :crash[_scheduler]
55
+ :on_error => :ignore
56
+ :on_error => ...
57
+ [o] on_error: what about TimeoutError in that scheme?
58
+ TimeoutError goes to $stderr, like a normal error
59
+ [o] link to SO for support
60
+ - sublink to "how to report bugs effectively"
61
+ [o] link to #ruote for support
62
+ [x] lockblock? pass a block to teach the scheduler how to lock?
63
+ is not necessary, @scheduler = Scheduler.new if should_start?
64
+ the surrounding Ruby code checks
65
+ [o] introduce job "vars", as in
66
+ http://stackoverflow.com/questions/18202848/how-to-have-a-variable-that-will-available-to-particular-scheduled-task-whenever
67
+ or job['key'] Job #[] and #[]=, as with Thread #[] #[]=
68
+ job-local variables #keys #key?
69
+ [o] thread-safety for job-local variables?
70
+ [x] discard past? discard_past => true or => "1d"
71
+ default would be discard_past => "1m" or scheduler freq * 2 ?
72
+ jobs would adjust their next_time until it fits the window...
73
+ ~~ discard past by default
74
+ [o] expanded block/schedulable (it's "callable")
75
+ ```
76
+ scheduler.every '10m' do
77
+ def pre
78
+ return false if Backend.down?
79
+ # ...
80
+ end
81
+ def post
82
+ # ...
83
+ end
84
+ def trigger
85
+ puts "oh hai!"
86
+ end
87
+ end
88
+ ```
89
+ or something like that...
90
+ ...
91
+ OR accept a class (and instantiate it the first time)
92
+ ```
93
+ scheduler.every '10m', Class.new do
94
+ def call(job, time)
95
+ # ...
96
+ end
97
+ end
98
+ ```
99
+ the job contains the instance in its @callable
100
+ [x] add spec case for corner case in Job#trigger (overlap vs reschedule) !!!
101
+ [o] rethink job array vs job set for #scheduled?
102
+ [x] introduce common parent class for EveryJob and IntervalJob
103
+ [o] create spec/ at_job_spec.rb, repeat_job_spec.rb, cron_job_spec.rb, ...
104
+ [x] ensure EveryJob do not schedule in the past (it's already like that)
105
+ [o] CronLine#next_time should return a time with subseconds chopped off
106
+ [o] drop min work threads setting?
107
+ [o] thread pool something? Thread upper limit?
108
+ [o] Rufus::Scheduler.singleton, Rufus::Scheduler.s
109
+ [o] EveryJob#first_at= and IntervalJob#first_at= should alter @next_time
110
+ [o] scheduler.schedule duration/time/cron ... for at/in/cron
111
+ (not every, nor interval)
112
+ scheduler.repeat time/cron ... for every/cron
113
+
114
+ [o] :lockfile => x, timestamp, process_id, thread_id...
115
+ warning: have to clean up that file on exit... or does the scheduler
116
+ timestamps it?
117
+ [ ] develop lockfile timestamp thinggy
118
+ ~ if the timestamp is too old (twice the default frequency?) then
119
+ lock [file] take over...
120
+ Is that really what we want all the time?
121
+
122
+ [ ] idea: :mutex => x and :skip_on_mutex => true ?
123
+ would prevent blocking/waiting for the mutex to get available
124
+ :mutex => [ "mutex_name", true ]
125
+ :mutex => [ [ "mutex_name", true ], [ "other_mutex_name", false ] ]
126
+
127
+ [ ] bring back EM (but only EM.defer ?) :defer => true (Job or Scheduler
128
+ or both option?)
129
+
130
+ [ ] prepare a daemon, trust daemon-kit for that
131
+
132
+ [ ] :if => lambda { |job, time| ... } why not?
133
+ :unless => lambda { ...
134
+ :block => lambda { ...
135
+ can help get the block themselves leaner
136
+ #
137
+ investigate guards for schedulables... def if_guard; ...; end
138
+
139
+ [ ] scheduler.every '10', Class.new do
140
+ def call(job, time)
141
+ # might fail...
142
+ end
143
+ def on_error(err, job)
144
+ # catches...
145
+ end
146
+ end
57
147
 
@@ -22,41 +22,517 @@
22
22
  # Made in Japan.
23
23
  #++
24
24
 
25
+ require 'date' if RUBY_VERSION < '1.9.0'
26
+ require 'time'
27
+ require 'thread'
28
+ require 'tzinfo'
29
+ require 'fileutils'
25
30
 
26
- require 'rufus/sc/scheduler'
27
31
 
32
+ module Rufus
28
33
 
29
- module Rufus::Scheduler
34
+ class Scheduler
30
35
 
31
- # Starts and return a new instance of a PlainScheduler.
32
- #
33
- def self.new(opts={})
36
+ require 'rufus/scheduler/util'
37
+ require 'rufus/scheduler/jobs'
38
+ require 'rufus/scheduler/cronline'
39
+ require 'rufus/scheduler/job_array'
34
40
 
35
- PlainScheduler.start_new(opts)
36
- end
41
+ VERSION = '3.0.0'
42
+
43
+ #
44
+ # This error is thrown when the :timeout attribute triggers
45
+ #
46
+ class TimeoutError < StandardError; end
47
+
48
+ #MIN_WORK_THREADS = 7
49
+ MAX_WORK_THREADS = 35
50
+
51
+ attr_accessor :frequency
52
+ attr_reader :started_at
53
+ attr_reader :thread
54
+ attr_reader :thread_key
55
+ attr_reader :mutexes
56
+
57
+ #attr_accessor :min_work_threads
58
+ attr_accessor :max_work_threads
59
+
60
+ attr_accessor :stderr
61
+
62
+ attr_reader :work_queue
63
+
64
+ def initialize(opts={})
65
+
66
+ @opts = opts
67
+
68
+ @started_at = nil
69
+ @paused = false
70
+
71
+ @jobs = JobArray.new
72
+
73
+ @frequency = Rufus::Scheduler.parse(opts[:frequency] || 0.300)
74
+ @mutexes = {}
75
+
76
+ @work_queue = Queue.new
37
77
 
38
- # A quick way to get a scheduler up an running
39
- #
40
- # require 'rubygems'
41
- # s = Rufus::Scheduler.start_new
42
- #
43
- # If EventMachine is present and running will create an EmScheduler, else
44
- # it will create a PlainScheduler instance.
45
- #
46
- def self.start_new(opts={})
47
-
48
- if defined?(EM) and EM.reactor_running?
49
- EmScheduler.start_new(opts)
50
- else
51
- PlainScheduler.start_new(opts)
78
+ #@min_work_threads = opts[:min_work_threads] || MIN_WORK_THREADS
79
+ @max_work_threads = opts[:max_work_threads] || MAX_WORK_THREADS
80
+
81
+ @stderr = $stderr
82
+
83
+ @thread_key = "rufus_scheduler_#{self.object_id}"
84
+
85
+ consider_lockfile || return
86
+
87
+ start
88
+ end
89
+
90
+ # Returns a singleton Rufus::Scheduler instance
91
+ #
92
+ def self.singleton(opts={})
93
+
94
+ @singleton ||= Rufus::Scheduler.new(opts)
95
+ end
96
+
97
+ # Alias for Rufus::Scheduler.singleton
98
+ #
99
+ def self.s(opts={}); singleton(opts); end
100
+
101
+ # Releasing the gem would probably require redirecting .start_new to
102
+ # .new and emit a simple deprecation message.
103
+ #
104
+ # For now, let's assume the people pointing at rufus-scheduler/master
105
+ # on GitHub know what they do...
106
+ #
107
+ def self.start_new
108
+
109
+ fail "this is rufus-scheduler 3.0, use .new instead of .start_new"
110
+ end
111
+
112
+ def shutdown(opt=nil)
113
+
114
+ @started_at = nil
115
+
116
+ jobs.each { |j| j.unschedule }
117
+
118
+ @work_queue.clear
119
+
120
+ if opt == :wait
121
+ join_all_work_threads
122
+ elsif opt == :kill
123
+ kill_all_work_threads
124
+ end
125
+
126
+ @lockfile.flock(File::LOCK_UN) if @lockfile
127
+ end
128
+
129
+ alias stop shutdown
130
+
131
+ def uptime
132
+
133
+ @started_at ? Time.now - @started_at : nil
134
+ end
135
+
136
+ def uptime_s
137
+
138
+ self.class.to_duration(uptime)
139
+ end
140
+
141
+ def join
142
+
143
+ @thread.join
144
+ end
145
+
146
+ def paused?
147
+
148
+ @paused
149
+ end
150
+
151
+ def pause
152
+
153
+ @paused = true
154
+ end
155
+
156
+ def resume
157
+
158
+ @paused = false
52
159
  end
53
- end
54
160
 
55
- # Returns true if the given string seems to be a cron string.
56
- #
57
- def self.is_cron_string(s)
161
+ #--
162
+ # scheduling methods
163
+ #++
164
+
165
+ def at(time, callable=nil, opts={}, &block)
166
+
167
+ do_schedule(:once, time, callable, opts, opts[:job], block)
168
+ end
169
+
170
+ def schedule_at(time, callable=nil, opts={}, &block)
171
+
172
+ do_schedule(:once, time, callable, opts, true, block)
173
+ end
174
+
175
+ def in(duration, callable=nil, opts={}, &block)
176
+
177
+ do_schedule(:once, duration, callable, opts, opts[:job], block)
178
+ end
179
+
180
+ def schedule_in(duration, callable=nil, opts={}, &block)
181
+
182
+ do_schedule(:once, duration, callable, opts, true, block)
183
+ end
184
+
185
+ def every(duration, callable=nil, opts={}, &block)
186
+
187
+ do_schedule(:every, duration, callable, opts, opts[:job], block)
188
+ end
189
+
190
+ def schedule_every(duration, callable=nil, opts={}, &block)
191
+
192
+ do_schedule(:every, duration, callable, opts, true, block)
193
+ end
194
+
195
+ def interval(duration, callable=nil, opts={}, &block)
196
+
197
+ do_schedule(:interval, duration, callable, opts, opts[:job], block)
198
+ end
199
+
200
+ def schedule_interval(duration, callable=nil, opts={}, &block)
201
+
202
+ do_schedule(:interval, duration, callable, opts, true, block)
203
+ end
204
+
205
+ def cron(cronline, callable=nil, opts={}, &block)
206
+
207
+ do_schedule(:cron, cronline, callable, opts, opts[:job], block)
208
+ end
209
+
210
+ def schedule_cron(cronline, callable=nil, opts={}, &block)
211
+
212
+ do_schedule(:cron, cronline, callable, opts, true, block)
213
+ end
214
+
215
+ def schedule(arg, callable=nil, opts={}, &block)
216
+
217
+ # TODO: eventually, spare one parse call
218
+
219
+ case Scheduler.parse(arg)
220
+ when CronLine then schedule_cron(arg, callable, opts, &block)
221
+ when Time then schedule_at(arg, callable, opts, &block)
222
+ else schedule_in(arg, callable, opts, &block)
223
+ end
224
+ end
225
+
226
+ def repeat(arg, callable=nil, opts={}, &block)
227
+
228
+ # TODO: eventually, spare one parse call
229
+
230
+ case Scheduler.parse(arg)
231
+ when CronLine then schedule_cron(arg, callable, opts, &block)
232
+ else schedule_every(arg, callable, opts, &block)
233
+ end
234
+ end
235
+
236
+ def unschedule(job_or_job_id)
237
+
238
+ job, job_id = fetch(job_or_job_id)
239
+
240
+ fail ArgumentError.new("no job found with id '#{job_id}'") unless job
241
+
242
+ job.unschedule if job
243
+ end
244
+
245
+ #--
246
+ # jobs methods
247
+ #++
248
+
249
+ # Returns all the scheduled jobs
250
+ # (even those right before re-schedule).
251
+ #
252
+ def jobs(opts={})
253
+
254
+ opts = { opts => true } if opts.is_a?(Symbol)
255
+
256
+ jobs = @jobs.to_a
257
+
258
+ if opts[:running]
259
+ jobs = jobs.select { |j| j.running? }
260
+ elsif ! opts[:all]
261
+ jobs = jobs.reject { |j| j.next_time.nil? || j.unscheduled_at }
262
+ end
263
+
264
+ tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
265
+ jobs = jobs.reject { |j| tags.find { |t| ! j.tags.include?(t) } }
266
+
267
+ jobs
268
+ end
58
269
 
59
- s.match(/.+ .+ .+ .+ .+/) # well...
270
+ def at_jobs(opts={})
271
+
272
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::AtJob) }
273
+ end
274
+
275
+ def in_jobs(opts={})
276
+
277
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::InJob) }
278
+ end
279
+
280
+ def every_jobs(opts={})
281
+
282
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::EveryJob) }
283
+ end
284
+
285
+ def interval_jobs(opts={})
286
+
287
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::IntervalJob) }
288
+ end
289
+
290
+ def cron_jobs(opts={})
291
+
292
+ jobs(opts).select { |j| j.is_a?(Rufus::Scheduler::CronJob) }
293
+ end
294
+
295
+ def job(job_id)
296
+
297
+ @jobs[job_id]
298
+ end
299
+
300
+ # Returns true if this job is currently scheduled.
301
+ #
302
+ # Takes extra care to answer true if the job is a repeat job
303
+ # currently firing.
304
+ #
305
+ def scheduled?(job_or_job_id)
306
+
307
+ job, job_id = fetch(job_or_job_id)
308
+
309
+ !! (job && job.next_time != nil)
310
+ end
311
+
312
+ # Lists all the threads associated with this scheduler.
313
+ #
314
+ def threads
315
+
316
+ Thread.list.select { |t| t[thread_key] }
317
+ end
318
+
319
+ # Lists all the work threads (the ones actually running the scheduled
320
+ # block code)
321
+ #
322
+ # Accepts a query option, which can be set to:
323
+ # * :all (default), returns all the threads that are work threads
324
+ # or are currently running a job
325
+ # * :active, returns all threads that are currenly running a job
326
+ # * :vacant, returns the threads that are not running a job
327
+ #
328
+ # If, thanks to :blocking => true, a job is scheduled to monopolize the
329
+ # main scheduler thread, that thread will get returned when :active or
330
+ # :all.
331
+ #
332
+ def work_threads(query=:all)
333
+
334
+ ts =
335
+ threads.select { |t|
336
+ t[:rufus_scheduler_job] || t[:rufus_scheduler_work_thread]
337
+ }
338
+
339
+ case query
340
+ when :active then ts.select { |t| t[:rufus_scheduler_job] }
341
+ when :vacant then ts.reject { |t| t[:rufus_scheduler_job] }
342
+ else ts
343
+ end
344
+ end
345
+
346
+ def running_jobs(opts={})
347
+
348
+ jobs(opts.merge(:running => true))
349
+ end
350
+
351
+ def on_error(job, err)
352
+
353
+ pre = err.object_id.to_s
354
+
355
+ stderr.puts("{ #{pre} rufus-scheduler intercepted an error:")
356
+ stderr.puts(" #{pre} job:")
357
+ stderr.puts(" #{pre} #{job.class} #{job.original.inspect} #{job.opts.inspect}")
358
+ stderr.puts(" #{pre} error:")
359
+ stderr.puts(" #{pre} #{err.object_id}")
360
+ stderr.puts(" #{pre} #{err.class}")
361
+ stderr.puts(" #{pre} #{err}")
362
+ err.backtrace.each do |l|
363
+ stderr.puts(" #{pre} #{l}")
364
+ end
365
+ stderr.puts("} #{pre} .")
366
+
367
+ rescue => e
368
+
369
+ stderr.puts("failure in #on_error itself:")
370
+ stderr.puts(e.inspect)
371
+ stderr.puts(e.backtrace)
372
+
373
+ ensure
374
+
375
+ stderr.flush
376
+ end
377
+
378
+ protected
379
+
380
+ # Returns [ job, job_id ]
381
+ #
382
+ def fetch(job_or_job_id)
383
+
384
+ if job_or_job_id.respond_to?(:job_id)
385
+ [ job_or_job_id, job_or_job_id.job_id ]
386
+ else
387
+ [ job(job_or_job_id), job_or_job_id ]
388
+ end
389
+ end
390
+
391
+ def consider_lockfile
392
+
393
+ @lockfile = nil
394
+
395
+ return true unless f = @opts[:lockfile]
396
+
397
+ raise ArgumentError.new(
398
+ ":lockfile argument must be a string, not a #{f.class}"
399
+ ) unless f.is_a?(String)
400
+
401
+ FileUtils.mkdir_p(File.dirname(f))
402
+
403
+ f = File.new(f, File::RDWR | File::CREAT)
404
+ locked = f.flock(File::LOCK_NB | File::LOCK_EX)
405
+
406
+ return false unless locked
407
+
408
+ now = Time.now
409
+
410
+ f.print("pid: #{$$}, ")
411
+ f.print("scheduler.object_id: #{self.object_id}, ")
412
+ f.print("time: #{now}, ")
413
+ f.print("timestamp: #{now.to_f}")
414
+ f.flush
415
+
416
+ @lockfile = f
417
+
418
+ true
419
+ end
420
+
421
+ def terminate_all_jobs
422
+
423
+ jobs.each { |j| j.unschedule }
424
+
425
+ sleep 0.01 while running_jobs.size > 0
426
+ end
427
+
428
+ def join_all_work_threads
429
+
430
+ work_threads.size.times { @work_queue << :sayonara }
431
+
432
+ work_threads.each { |t| t.join }
433
+
434
+ @work_queue.clear
435
+ end
436
+
437
+ def kill_all_work_threads
438
+
439
+ work_threads.each { |t| t.kill }
440
+ end
441
+
442
+ #def free_all_work_threads
443
+ #
444
+ # work_threads.each { |t| t.raise(KillSignal) }
445
+ #end
446
+
447
+ def start
448
+
449
+ @started_at = Time.now
450
+
451
+ @thread =
452
+ Thread.new do
453
+
454
+ while @started_at do
455
+
456
+ unschedule_jobs
457
+ trigger_jobs unless @paused
458
+ timeout_jobs
459
+
460
+ sleep(@frequency)
461
+ end
462
+ end
463
+
464
+ @thread[@thread_key] = true
465
+ @thread[:rufus_scheduler] = self
466
+ @thread[:name] = @opts[:thread_name] || "#{@thread_key}_scheduler"
467
+ end
468
+
469
+ def unschedule_jobs
470
+
471
+ @jobs.delete_unscheduled
472
+ end
473
+
474
+ def trigger_jobs
475
+
476
+ now = Time.now
477
+
478
+ @jobs.each(now) do |job|
479
+
480
+ job.trigger(now)
481
+ end
482
+ end
483
+
484
+ def timeout_jobs
485
+
486
+ work_threads(:active).each do |t|
487
+
488
+ job = t[:rufus_scheduler_job]
489
+ to = t[:rufus_scheduler_timeout]
490
+
491
+ next unless job && to
492
+ # thread might just have become inactive (job -> nil)
493
+
494
+ ts = t[:rufus_scheduler_time]
495
+ to = to.is_a?(Time) ? to : ts + to
496
+
497
+ next if to > Time.now
498
+
499
+ t.raise(Rufus::Scheduler::TimeoutError)
500
+ end
501
+ end
502
+
503
+ def do_schedule(job_type, t, callable, opts, return_job_instance, block)
504
+
505
+ raise RuntimeError.new(
506
+ 'cannot schedule, scheduler is down or shutting down'
507
+ ) if @started_at == nil
508
+
509
+ callable, opts = nil, callable if callable.is_a?(Hash)
510
+ return_job_instance ||= opts[:job]
511
+
512
+ job_class =
513
+ case job_type
514
+ when :once
515
+ tt = Rufus::Scheduler.parse(t)
516
+ tt.is_a?(Time) ? AtJob : InJob
517
+ when :every
518
+ EveryJob
519
+ when :interval
520
+ IntervalJob
521
+ when :cron
522
+ CronJob
523
+ end
524
+
525
+ job = job_class.new(self, t, opts, block || callable)
526
+
527
+ raise ArgumentError.new(
528
+ "job frequency (#{job.frequency}) is higher than " +
529
+ "scheduler frequency (#{@frequency})"
530
+ ) if job.respond_to?(:frequency) && job.frequency < @frequency
531
+
532
+ @jobs.push(job)
533
+
534
+ return_job_instance ? job : job.job_id
535
+ end
60
536
  end
61
537
  end
62
538