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
@@ -1,363 +0,0 @@
1
- #--
2
- # Copyright (c) 2005-2013, John Mettraux, jmettraux@gmail.com
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining a copy
5
- # of this software and associated documentation files (the "Software"), to deal
6
- # in the Software without restriction, including without limitation the rights
7
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the Software is
9
- # furnished to do so, subject to the following conditions:
10
- #
11
- # The above copyright notice and this permission notice shall be included in
12
- # all copies or substantial portions of the Software.
13
- #
14
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
- # THE SOFTWARE.
21
- #
22
- # Hecho en Costa Rica
23
- #++
24
-
25
-
26
- require 'date'
27
-
28
-
29
- module Rufus
30
-
31
- #--
32
- #
33
- # keeping that as a note.
34
- #
35
- #require 'tzinfo'
36
- #def time_zone(time)
37
- # offset = time.utc_offset / 3600
38
- # offset = offset < 0 ? offset.to_s : "+#{offset}"
39
- # TZInfo::Timezone.get("Etc/GMT#{offset}")
40
- #end
41
- #def timeshift(time, tz)
42
- # tz = TZInfo::Timezone.get(tz) unless tz.is_a?(TZInfo::Timezone)
43
- # t = tz.utc_to_local(time.getutc)
44
- # Time.parse(t.to_s[0..-5])
45
- #end
46
- #++
47
-
48
- # Returns the current time as an ISO date string
49
- #
50
- def Rufus.now
51
-
52
- to_iso8601_date(Time.new)
53
- end
54
-
55
- # As the name implies.
56
- #
57
- def Rufus.to_iso8601_date(date)
58
-
59
- date = case date
60
- when Date then date
61
- when Float then to_datetime(Time.at(date))
62
- when Time then to_datetime(date)
63
- else DateTime.parse(date)
64
- end
65
-
66
- s = date.to_s # this is costly
67
- s[10] = ' '
68
-
69
- s
70
- end
71
-
72
- # the old method we used to generate our ISO datetime strings
73
- #
74
- def Rufus.time_to_iso8601_date(time)
75
-
76
- s = time.getutc.strftime(TIME_FORMAT)
77
- o = time.utc_offset / 3600
78
- o = "#{o}00"
79
- o = "0#{o}" if o.length < 4
80
- o = "+#{o}" unless o[0..1] == '-'
81
-
82
- "#{s} #{o}"
83
- end
84
-
85
- # Returns a Ruby time
86
- #
87
- def Rufus.to_ruby_time(sdate)
88
-
89
- DateTime.parse(sdate)
90
- end
91
-
92
- # Equivalent to java.lang.System.currentTimeMillis()
93
- #
94
- def Rufus.current_time_millis
95
-
96
- (Time.new.to_f * 1000).to_i
97
- end
98
-
99
- # Turns a string like '1m10s' into a float like '70.0', more formally,
100
- # turns a time duration expressed as a string into a Float instance
101
- # (millisecond count).
102
- #
103
- # w -> week
104
- # d -> day
105
- # h -> hour
106
- # m -> minute
107
- # s -> second
108
- # M -> month
109
- # y -> year
110
- # 'nada' -> millisecond
111
- #
112
- # Some examples :
113
- #
114
- # Rufus.parse_time_string "0.5" # => 0.5
115
- # Rufus.parse_time_string "500" # => 0.5
116
- # Rufus.parse_time_string "1000" # => 1.0
117
- # Rufus.parse_time_string "1h" # => 3600.0
118
- # Rufus.parse_time_string "1h10s" # => 3610.0
119
- # Rufus.parse_time_string "1w2d" # => 777600.0
120
- #
121
- # Note will call #to_s on the input "string", so anything that is a String
122
- # or responds to #to_s will be OK.
123
- #
124
- def self.parse_time_string(string)
125
-
126
- string = string.to_s
127
-
128
- return 0.0 if string == ''
129
-
130
- m = string.match(/^(-?)([\d\.#{DURATION_LETTERS}]+)$/)
131
-
132
- raise ArgumentError.new("cannot parse '#{string}'") unless m
133
-
134
- mod = m[1] == '-' ? -1.0 : 1.0
135
- val = 0.0
136
-
137
- s = m[2]
138
-
139
- while s.length > 0
140
- m = nil
141
- if m = s.match(/^(\d+|\d+\.\d*|\d*\.\d+)([#{DURATION_LETTERS}])(.*)$/)
142
- val += m[1].to_f * DURATIONS[m[2]]
143
- elsif s.match(/^\d+$/)
144
- val += s.to_i / 1000.0
145
- elsif s.match(/^\d*\.\d*$/)
146
- val += s.to_f
147
- else
148
- raise ArgumentError.new("cannot parse '#{string}' (especially '#{s}')")
149
- end
150
- break unless m && m[3]
151
- s = m[3]
152
- end
153
-
154
- mod * val
155
- end
156
-
157
- class << self
158
- alias_method :parse_duration_string, :parse_time_string
159
- end
160
-
161
- #--
162
- # conversion methods between Date[Time] and Time
163
- #++
164
-
165
- #--
166
- # Ruby Cookbook 1st edition p.111
167
- # http://www.oreilly.com/catalog/rubyckbk/
168
- # a must
169
- #++
170
-
171
- # Converts a Time instance to a DateTime one
172
- #
173
- def Rufus.to_datetime(time)
174
-
175
- s = time.sec + Rational(time.usec, 10**6)
176
- o = Rational(time.utc_offset, 3600 * 24)
177
-
178
- begin
179
-
180
- DateTime.new(time.year, time.month, time.day, time.hour, time.min, s, o)
181
-
182
- rescue Exception => e
183
-
184
- DateTime.new(
185
- time.year,
186
- time.month,
187
- time.day,
188
- time.hour,
189
- time.min,
190
- time.sec,
191
- time.utc_offset)
192
- end
193
- end
194
-
195
- def Rufus.to_gm_time(dtime)
196
-
197
- to_ttime(dtime.new_offset, :gm)
198
- end
199
-
200
- def Rufus.to_local_time(dtime)
201
-
202
- to_ttime(dtime.new_offset(DateTime.now.offset - offset), :local)
203
- end
204
-
205
- def Rufus.to_ttime(d, method)
206
-
207
- usec = (d.sec_fraction * 3600 * 24 * (10**6)).to_i
208
- Time.send(method, d.year, d.month, d.day, d.hour, d.min, d.sec, usec)
209
- end
210
-
211
- # Turns a number of seconds into a a time string
212
- #
213
- # Rufus.to_duration_string 0 # => '0s'
214
- # Rufus.to_duration_string 60 # => '1m'
215
- # Rufus.to_duration_string 3661 # => '1h1m1s'
216
- # Rufus.to_duration_string 7 * 24 * 3600 # => '1w'
217
- # Rufus.to_duration_string 30 * 24 * 3600 + 1 # => "4w2d1s"
218
- #
219
- # It goes from seconds to the year. Months are not counted (as they
220
- # are of variable length). Weeks are counted.
221
- #
222
- # For 30 days months to be counted, the second parameter of this
223
- # method can be set to true.
224
- #
225
- # Rufus.to_time_string 30 * 24 * 3600 + 1, true # => "1M1s"
226
- #
227
- # (to_time_string is an alias for to_duration_string)
228
- #
229
- # If a Float value is passed, milliseconds will be displayed without
230
- # 'marker'
231
- #
232
- # Rufus.to_duration_string 0.051 # =>"51"
233
- # Rufus.to_duration_string 7.051 # =>"7s51"
234
- # Rufus.to_duration_string 0.120 + 30 * 24 * 3600 + 1 # =>"4w2d1s120"
235
- #
236
- # (this behaviour mirrors the one found for parse_time_string()).
237
- #
238
- # Options are :
239
- #
240
- # * :months, if set to true, months (M) of 30 days will be taken into
241
- # account when building up the result
242
- # * :drop_seconds, if set to true, seconds and milliseconds will be trimmed
243
- # from the result
244
- #
245
- def Rufus.to_duration_string(seconds, options={})
246
-
247
- return (options[:drop_seconds] ? '0m' : '0s') if seconds <= 0
248
-
249
- h = to_duration_hash(seconds, options)
250
-
251
- s = DU_KEYS.inject('') { |r, key|
252
- count = h[key]
253
- count = nil if count == 0
254
- r << "#{count}#{key}" if count
255
- r
256
- }
257
-
258
- ms = h[:ms]
259
- s << ms.to_s if ms
260
-
261
- s
262
- end
263
-
264
- class << self
265
- alias_method :to_time_string, :to_duration_string
266
- end
267
-
268
- # Turns a number of seconds (integer or Float) into a hash like in :
269
- #
270
- # Rufus.to_duration_hash 0.051
271
- # # => { :ms => "51" }
272
- # Rufus.to_duration_hash 7.051
273
- # # => { :s => 7, :ms => "51" }
274
- # Rufus.to_duration_hash 0.120 + 30 * 24 * 3600 + 1
275
- # # => { :w => 4, :d => 2, :s => 1, :ms => "120" }
276
- #
277
- # This method is used by to_duration_string (to_time_string) behind
278
- # the scene.
279
- #
280
- # Options are :
281
- #
282
- # * :months, if set to true, months (M) of 30 days will be taken into
283
- # account when building up the result
284
- # * :drop_seconds, if set to true, seconds and milliseconds will be trimmed
285
- # from the result
286
- #
287
- def Rufus.to_duration_hash(seconds, options={})
288
-
289
- h = {}
290
-
291
- if seconds.is_a?(Float)
292
- h[:ms] = (seconds % 1 * 1000).to_i
293
- seconds = seconds.to_i
294
- end
295
-
296
- if options[:drop_seconds]
297
- h.delete(:ms)
298
- seconds = (seconds - seconds % 60)
299
- end
300
-
301
- durations = options[:months] ? DURATIONS2M : DURATIONS2
302
-
303
- durations.each do |key, duration|
304
-
305
- count = seconds / duration
306
- seconds = seconds % duration
307
-
308
- h[key.to_sym] = count if count > 0
309
- end
310
-
311
- h
312
- end
313
-
314
- # Ensures that a duration is a expressed as a Float instance.
315
- #
316
- # duration_to_f("10s")
317
- #
318
- # will yield 10.0
319
- #
320
- def Rufus.duration_to_f(s)
321
-
322
- return s if s.kind_of?(Float)
323
- return parse_time_string(s) if s.kind_of?(String)
324
- Float(s.to_s)
325
- end
326
-
327
- # Ensures an 'at' value is translated to a float
328
- # (to be compared with the float coming from time.to_f)
329
- #
330
- def Rufus.at_to_f(at)
331
-
332
- # TODO : use chronic if present
333
-
334
- at = to_ruby_time(at) if at.is_a?(String)
335
- at = to_gm_time(at) if at.is_a?(DateTime)
336
- #at = at.to_f if at.is_a?(Time)
337
- at = at.to_f if at.respond_to?(:to_f)
338
-
339
- raise ArgumentError.new(
340
- "cannot determine 'at' time from : #{at.inspect}"
341
- ) unless at.is_a?(Float)
342
-
343
- at
344
- end
345
-
346
- DURATIONS2M = [
347
- [ 'y', 365 * 24 * 3600 ],
348
- [ 'M', 30 * 24 * 3600 ],
349
- [ 'w', 7 * 24 * 3600 ],
350
- [ 'd', 24 * 3600 ],
351
- [ 'h', 3600 ],
352
- [ 'm', 60 ],
353
- [ 's', 1 ]
354
- ]
355
- DURATIONS2 = DURATIONS2M.dup
356
- DURATIONS2.delete_at(1)
357
-
358
- DURATIONS = DURATIONS2M.inject({}) { |r, (k, v)| r[k] = v; r }
359
- DURATION_LETTERS = DURATIONS.keys.join
360
-
361
- DU_KEYS = DURATIONS2M.collect { |k, v| k.to_sym }
362
- end
363
-
@@ -1,636 +0,0 @@
1
- #--
2
- # Copyright (c) 2006-2013, John Mettraux, jmettraux@gmail.com
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining a copy
5
- # of this software and associated documentation files (the "Software"), to deal
6
- # in the Software without restriction, including without limitation the rights
7
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the Software is
9
- # furnished to do so, subject to the following conditions:
10
- #
11
- # The above copyright notice and this permission notice shall be included in
12
- # all copies or substantial portions of the Software.
13
- #
14
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
- # THE SOFTWARE.
21
- #
22
- # Made in Japan.
23
- #++
24
-
25
-
26
- require 'rufus/sc/version'
27
- require 'rufus/sc/rtime'
28
- require 'rufus/sc/cronline'
29
- require 'rufus/sc/jobs'
30
- require 'rufus/sc/jobqueues'
31
-
32
-
33
- module Rufus::Scheduler
34
-
35
- #
36
- # It's OK to pass an object responding to :trigger when scheduling a job
37
- # (instead of passing a block).
38
- #
39
- # This is simply a helper module. The rufus-scheduler will check if scheduled
40
- # object quack (respond to :trigger anyway).
41
- #
42
- module Schedulable
43
- def call(job)
44
- trigger(job.params)
45
- end
46
- def trigger(params)
47
- raise NotImplementedError.new('implementation is missing')
48
- end
49
- end
50
-
51
- #
52
- # For backward compatibility
53
- #
54
- module ::Rufus::Schedulable
55
- extend ::Rufus::Scheduler::Schedulable
56
- end
57
-
58
- # Legacy from the previous version of Rufus-Scheduler.
59
- #
60
- # Consider all methods here as 'deprecated'.
61
- #
62
- module LegacyMethods
63
-
64
- def find_jobs(tag=nil)
65
- tag ? find_by_tag(tag) : all_jobs.values
66
- end
67
- def at_job_count
68
- @jobs.select(:at).size +
69
- @jobs.select(:in).size
70
- end
71
- def every_job_count
72
- @jobs.select(:every).size
73
- end
74
- def cron_job_count
75
- @cron_jobs.size
76
- end
77
- def pending_job_count
78
- @jobs.size
79
- end
80
- def precision
81
- @frequency
82
- end
83
- end
84
-
85
- #
86
- # The core of a rufus-scheduler. See implementations like
87
- # Rufus::Scheduler::PlainScheduler and Rufus::Scheduler::EmScheduler for
88
- # directly usable stuff.
89
- #
90
- class SchedulerCore
91
-
92
- include LegacyMethods
93
-
94
- # classical options hash
95
- #
96
- attr_reader :options
97
-
98
- # Instantiates a Rufus::Scheduler.
99
- #
100
- def initialize(opts={})
101
-
102
- @options = opts
103
-
104
- @jobs = get_queue(:at, opts)
105
- @cron_jobs = get_queue(:cron, opts)
106
-
107
- @frequency = @options[:frequency] || 0.330
108
-
109
- @mutexes = {}
110
- end
111
-
112
- # Instantiates and starts a new Rufus::Scheduler.
113
- #
114
- def self.start_new(opts={})
115
-
116
- s = self.new(opts)
117
- s.start
118
- s
119
- end
120
-
121
- #--
122
- # SCHEDULE METHODS
123
- #++
124
-
125
- # Schedules a job in a given amount of time.
126
- #
127
- # scheduler.in '20m' do
128
- # puts "order ristretto"
129
- # end
130
- #
131
- # will order an espresso (well sort of) in 20 minutes.
132
- #
133
- def in(t, s=nil, opts={}, &block)
134
-
135
- add_job(InJob.new(self, t, combine_opts(s, opts), &block))
136
- end
137
- alias :schedule_in :in
138
-
139
- # Schedules a job at a given point in time.
140
- #
141
- # scheduler.at 'Thu Mar 26 19:30:00 2009' do
142
- # puts 'order pizza'
143
- # end
144
- #
145
- # pizza is for Thursday at 2000 (if the shop brochure is right).
146
- #
147
- def at(t, s=nil, opts={}, &block)
148
-
149
- add_job(AtJob.new(self, t, combine_opts(s, opts), &block))
150
- end
151
- alias :schedule_at :at
152
-
153
- # Schedules a recurring job every t.
154
- #
155
- # scheduler.every '5m1w' do
156
- # puts 'check blood pressure'
157
- # end
158
- #
159
- # checking blood pressure every 5 months and 1 week.
160
- #
161
- def every(t, s=nil, opts={}, &block)
162
-
163
- add_job(EveryJob.new(self, t, combine_opts(s, opts), &block))
164
- end
165
- alias :schedule_every :every
166
-
167
- # Schedules a job given a cron string.
168
- #
169
- # scheduler.cron '0 22 * * 1-5' do
170
- # # every day of the week at 00:22
171
- # puts 'activate security system'
172
- # end
173
- #
174
- def cron(cronstring, s=nil, opts={}, &block)
175
-
176
- add_cron_job(CronJob.new(self, cronstring, combine_opts(s, opts), &block))
177
- end
178
- alias :schedule :cron
179
-
180
- # Unschedules a job (cron or at/every/in job).
181
- #
182
- # Returns the job that got unscheduled.
183
- #
184
- def unschedule(job_or_id)
185
-
186
- job_id = job_or_id.respond_to?(:job_id) ? job_or_id.job_id : job_or_id
187
-
188
- @jobs.unschedule(job_id) || @cron_jobs.unschedule(job_id)
189
- end
190
-
191
- # Given a tag, unschedules all the jobs that bear that tag.
192
- #
193
- def unschedule_by_tag(tag)
194
-
195
- jobs = find_by_tag(tag)
196
- jobs.each { |job| unschedule(job.job_id) }
197
-
198
- jobs
199
- end
200
-
201
- # Pauses a given job. If the argument is an id (String) and the
202
- # corresponding job cannot be found, an ArgumentError will get raised.
203
- #
204
- def pause(job_or_id)
205
-
206
- find(job_or_id).pause
207
- end
208
-
209
- # Resumes a given job. If the argument is an id (String) and the
210
- # corresponding job cannot be found, an ArgumentError will get raised.
211
- #
212
- def resume(job_or_id)
213
-
214
- find(job_or_id).resume
215
- end
216
-
217
- #--
218
- # MISC
219
- #++
220
-
221
- # Determines if there is #log_exception, #handle_exception or #on_exception
222
- # method. If yes, hands the exception to it, else defaults to outputting
223
- # details to $stderr.
224
- #
225
- def do_handle_exception(job, exception)
226
-
227
- begin
228
-
229
- [ :log_exception, :handle_exception, :on_exception ].each do |m|
230
-
231
- next unless self.respond_to?(m)
232
-
233
- if method(m).arity == 1
234
- self.send(m, exception)
235
- else
236
- self.send(m, job, exception)
237
- end
238
-
239
- return
240
- # exception was handled successfully
241
- end
242
-
243
- rescue Exception => e
244
-
245
- $stderr.puts '*' * 80
246
- $stderr.puts 'the exception handling method itself had an issue:'
247
- $stderr.puts e
248
- $stderr.puts *e.backtrace
249
- $stderr.puts '*' * 80
250
- end
251
-
252
- $stderr.puts '=' * 80
253
- $stderr.puts 'scheduler caught exception:'
254
- $stderr.puts exception
255
- $stderr.puts *exception.backtrace
256
- $stderr.puts '=' * 80
257
- end
258
-
259
- #--
260
- # JOB LOOKUP
261
- #++
262
-
263
- # Returns a map job_id => job for at/in/every jobs
264
- #
265
- def jobs
266
-
267
- @jobs.to_h
268
- end
269
-
270
- # Returns a map job_id => job for cron jobs
271
- #
272
- def cron_jobs
273
-
274
- @cron_jobs.to_h
275
- end
276
-
277
- # Returns a map job_id => job of all the jobs currently in the scheduler
278
- #
279
- def all_jobs
280
-
281
- jobs.merge(cron_jobs)
282
- end
283
-
284
- # Returns a list of jobs with the given tag
285
- #
286
- def find_by_tag(tag)
287
-
288
- all_jobs.values.select { |j| j.tags.include?(tag) }
289
- end
290
-
291
- # Mostly used to find a job given its id. If the argument is a job, will
292
- # simply return it.
293
- #
294
- # If the argument is an id, and no job with that id is found, it will
295
- # raise an ArgumentError.
296
- #
297
- def find(job_or_id)
298
-
299
- return job_or_id if job_or_id.respond_to?(:job_id)
300
-
301
- job = all_jobs[job_or_id]
302
-
303
- raise ArgumentError.new(
304
- "couldn't find job #{job_or_id.inspect}"
305
- ) unless job
306
-
307
- job
308
- end
309
-
310
- # Returns the current list of trigger threads (threads) dedicated to
311
- # the execution of jobs.
312
- #
313
- def trigger_threads
314
-
315
- Thread.list.select { |t|
316
- t["rufus_scheduler__trigger_thread__#{self.object_id}"]
317
- }
318
- end
319
-
320
- # Returns the list of the currently running jobs (jobs that just got
321
- # triggered and are executing).
322
- #
323
- def running_jobs
324
-
325
- Thread.list.collect { |t|
326
- t["rufus_scheduler__trigger_thread__#{self.object_id}"]
327
- }.compact
328
- end
329
-
330
- # This is a blocking call, it will return when all the jobs have been
331
- # unscheduled, waiting for any running one to finish before unscheduling
332
- # it.
333
- #
334
- def terminate_all_jobs
335
-
336
- all_jobs.each do |job_id, job|
337
- job.unschedule
338
- end
339
-
340
- while running_jobs.size > 0
341
- sleep 0.01
342
- end
343
- end
344
-
345
- protected
346
-
347
- # Returns a job queue instance.
348
- #
349
- # (made it into a method for easy override)
350
- #
351
- def get_queue(type, opts)
352
-
353
- q = if type == :cron
354
- opts[:cron_job_queue] || Rufus::Scheduler::CronJobQueue.new
355
- else
356
- opts[:job_queue] || Rufus::Scheduler::JobQueue.new
357
- end
358
-
359
- q.scheduler = self if q.respond_to?(:scheduler=)
360
-
361
- q
362
- end
363
-
364
- def combine_opts(schedulable, opts)
365
-
366
- if schedulable.respond_to?(:trigger) || schedulable.respond_to?(:call)
367
-
368
- opts[:schedulable] = schedulable
369
-
370
- elsif schedulable != nil
371
-
372
- opts = schedulable.merge(opts)
373
- end
374
-
375
- opts
376
- end
377
-
378
- # The method that does the "wake up and trigger any job that should get
379
- # triggered.
380
- #
381
- def step
382
-
383
- @cron_jobs.trigger_matching_jobs
384
- @jobs.trigger_matching_jobs
385
- end
386
-
387
- def add_job(job)
388
-
389
- complain_if_blocking_and_timeout(job)
390
-
391
- return nil if job.params[:discard_past] && Time.now.to_f >= job.at
392
-
393
- @jobs << job
394
-
395
- job
396
- end
397
-
398
- def add_cron_job(job)
399
-
400
- complain_if_blocking_and_timeout(job)
401
-
402
- @cron_jobs << job
403
-
404
- job
405
- end
406
-
407
- # Raises an error if the job has the params :blocking and :timeout set
408
- #
409
- def complain_if_blocking_and_timeout(job)
410
-
411
- raise(
412
- ArgumentError.new('cannot set a :timeout on a :blocking job')
413
- ) if job.params[:blocking] and job.params[:timeout]
414
- end
415
-
416
- # The default, plain, implementation. If 'blocking' is true, will simply
417
- # call the block and return when the block is done.
418
- # Else, it will call the block in a dedicated thread.
419
- #
420
- # TODO : clarify, the blocking here blocks the whole scheduler, while
421
- # EmScheduler blocking triggers for the next tick. Not the same thing ...
422
- #
423
- def trigger_job(params, &block)
424
-
425
- if params[:blocking]
426
- block.call
427
- elsif m = params[:mutex]
428
- Thread.new { synchronize_with_mutex(m, &block) }
429
- else
430
- Thread.new { block.call }
431
- end
432
- end
433
-
434
- def synchronize_with_mutex(mutex, &block)
435
- case mutex
436
- when Mutex
437
- mutex.synchronize { block.call }
438
- when Array
439
- mutex.reduce(block) do |memo, m|
440
- m = (@mutexes[m.to_s] ||= Mutex.new) unless m.is_a?(Mutex)
441
- lambda { m.synchronize { memo.call } }
442
- end.call
443
- else
444
- (@mutexes[mutex.to_s] ||= Mutex.new).synchronize { block.call }
445
- end
446
- end
447
- end
448
-
449
- #--
450
- # SCHEDULER 'IMPLEMENTATIONS'
451
- #++
452
-
453
- #
454
- # A classical implementation, uses a sleep/step loop in a thread (like the
455
- # original rufus-scheduler).
456
- #
457
- class PlainScheduler < SchedulerCore
458
-
459
- def start
460
-
461
- @thread = Thread.new do
462
- loop do
463
- sleep(@frequency)
464
- step
465
- end
466
- end
467
-
468
- @thread[:name] =
469
- @options[:thread_name] ||
470
- "#{self.class} - #{Rufus::Scheduler::VERSION}"
471
- end
472
-
473
- # Stops this scheduler.
474
- #
475
- # == :terminate => true
476
- #
477
- # If the option :terminate is set to true,
478
- # the method will return once all the jobs have been unscheduled and
479
- # are done with their current run if any.
480
- #
481
- # (note that if a job is
482
- # currently running, this method will wait for it to terminate, it
483
- # will not interrupt the job run).
484
- #
485
- def stop(opts={})
486
-
487
- @thread.exit
488
-
489
- terminate_all_jobs if opts[:terminate]
490
- end
491
-
492
- def join
493
-
494
- @thread.join
495
- end
496
- end
497
-
498
- # TODO : investigate idea
499
- #
500
- #class BlockingScheduler < PlainScheduler
501
- # # use a Queue and a worker thread for the 'blocking' jobs
502
- #end
503
-
504
- #
505
- # A rufus-scheduler that steps only when the ruby process receives the
506
- # 10 / USR1 signal.
507
- #
508
- class SignalScheduler < SchedulerCore
509
-
510
- def initialize(opts={})
511
-
512
- super(opts)
513
-
514
- trap(@options[:signal] || 10) do
515
- step
516
- end
517
- end
518
-
519
-
520
- # Stops this scheduler.
521
- #
522
- # == :terminate => true
523
- #
524
- # If the option :terminate is set to true,
525
- # the method will return once all the jobs have been unscheduled and
526
- # are done with their current run if any.
527
- #
528
- # (note that if a job is
529
- # currently running, this method will wait for it to terminate, it
530
- # will not interrupt the job run).
531
- #
532
- def stop(opts={})
533
-
534
- trap(@options[:signal] || 10)
535
-
536
- terminate_all_jobs if opts[:terminate]
537
- end
538
- end
539
-
540
- #
541
- # A rufus-scheduler that uses an EventMachine periodic timer instead of a
542
- # loop.
543
- #
544
- class EmScheduler < SchedulerCore
545
-
546
- def initialize(opts={})
547
-
548
- raise LoadError.new(
549
- 'EventMachine missing, "require \'eventmachine\'" might help'
550
- ) unless defined?(EM)
551
-
552
- super(opts)
553
- end
554
-
555
- def start
556
-
557
- @em_thread = nil
558
-
559
- unless EM.reactor_running?
560
- @em_thread = Thread.new { EM.run }
561
- while (not EM.reactor_running?)
562
- Thread.pass
563
- end
564
- end
565
-
566
- #unless EM.reactor_running?
567
- # t = Thread.current
568
- # @em_thread = Thread.new { EM.run { t.wakeup } }
569
- # Thread.stop # EM will wake us up when it's ready
570
- #end
571
-
572
- @timer = EM::PeriodicTimer.new(@frequency) { step }
573
- end
574
-
575
- # Stops the scheduler.
576
- #
577
- # == :stop_em => true
578
- #
579
- # If the :stop_em option is passed and set to true, it will stop the
580
- # EventMachine (but only if it started the EM by itself !).
581
- #
582
- # == :terminate => true
583
- #
584
- # If the option :terminate is set to true,
585
- # the method will return once all the jobs have been unscheduled and
586
- # are done with their current run if any.
587
- #
588
- # (note that if a job is
589
- # currently running, this method will wait for it to terminate, it
590
- # will not interrupt the job run).
591
- #
592
- def stop(opts={})
593
-
594
- @timer.cancel
595
-
596
- terminate_all_jobs if opts[:terminate]
597
-
598
- EM.stop if opts[:stop_em] and @em_thread
599
- end
600
-
601
- # Joins this scheduler. Will actually join it only if it started the
602
- # underlying EventMachine.
603
- #
604
- def join
605
-
606
- @em_thread.join if @em_thread
607
- end
608
-
609
- protected
610
-
611
- # If 'blocking' is set to true, the block will get called at the
612
- # 'next_tick'. Else the block will get called via 'defer' (own thread).
613
- #
614
- def trigger_job(params, &block)
615
-
616
- # :next_tick monopolizes the EM
617
- # :defer executes its block in another thread
618
- # (if I read the doc carefully...)
619
-
620
- if params[:blocking]
621
- EM.next_tick { block.call }
622
- elsif m = params[:mutex]
623
- EM.defer { synchronize_with_mutex(m, &block) }
624
- else
625
- EM.defer { block.call }
626
- end
627
- end
628
- end
629
-
630
- #
631
- # This error is thrown when the :timeout attribute triggers
632
- #
633
- class TimeOutError < RuntimeError
634
- end
635
- end
636
-