rufus-scheduler 2.0.24 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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