rufus-scheduler 1.0.14 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,7 +19,7 @@
19
19
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
20
  # THE SOFTWARE.
21
21
  #
22
- # Hecho en Costa Rica.
22
+ # Hecho en Costa Rica
23
23
  #++
24
24
 
25
25
 
@@ -29,9 +29,6 @@ require 'date'
29
29
 
30
30
  module Rufus
31
31
 
32
- #TIME_FORMAT = "%Y-%m-%d %H:%M:%S"
33
-
34
- #
35
32
  # Returns the current time as an ISO date string
36
33
  #
37
34
  def Rufus.now
@@ -39,17 +36,15 @@ module Rufus
39
36
  to_iso8601_date(Time.new())
40
37
  end
41
38
 
42
- #
43
39
  # As the name implies.
44
40
  #
45
41
  def Rufus.to_iso8601_date (date)
46
42
 
47
- if date.kind_of? Float
48
- date = to_datetime(Time.at(date))
49
- elsif date.kind_of? Time
50
- date = to_datetime(date)
51
- elsif not date.kind_of? Date
52
- date = DateTime.parse(date)
43
+ date = case date
44
+ when Date then date
45
+ when Float then to_datetime(Time.at(date))
46
+ when Time then to_datetime(date)
47
+ else DateTime.parse(date)
53
48
  end
54
49
 
55
50
  s = date.to_s # this is costly
@@ -58,21 +53,19 @@ module Rufus
58
53
  s
59
54
  end
60
55
 
61
- #
62
56
  # the old method we used to generate our ISO datetime strings
63
57
  #
64
58
  def Rufus.time_to_iso8601_date (time)
65
59
 
66
60
  s = time.getutc().strftime(TIME_FORMAT)
67
61
  o = time.utc_offset / 3600
68
- o = o.to_s + '00'
69
- o = '0' + o if o.length < 4
70
- o = '+' + o unless o[0..1] == '-'
62
+ o = "#{o}00"
63
+ o = "0#{o}" if o.length < 4
64
+ o = "+#{o}" unless o[0..1] == '-'
71
65
 
72
- s + ' ' + o.to_s
66
+ "#{s} #{o}"
73
67
  end
74
68
 
75
- #
76
69
  # Returns a Ruby time
77
70
  #
78
71
  def Rufus.to_ruby_time (sdate)
@@ -80,18 +73,13 @@ module Rufus
80
73
  DateTime.parse(sdate)
81
74
  end
82
75
 
83
- #def Rufus.parse_date (date)
84
- #end
85
-
86
- #
87
- # equivalent to java.lang.System.currentTimeMillis()
76
+ # Equivalent to java.lang.System.currentTimeMillis()
88
77
  #
89
78
  def Rufus.current_time_millis
90
79
 
91
80
  (Time.new.to_f * 1000).to_i
92
81
  end
93
82
 
94
- #
95
83
  # Turns a string like '1m10s' into a float like '70.0', more formally,
96
84
  # turns a time duration expressed as a string into a Float instance
97
85
  # (millisecond count).
@@ -133,7 +121,6 @@ module Rufus
133
121
 
134
122
  c = string[index, 1]
135
123
 
136
- #if is_digit?(c)
137
124
  if (c >= '0' and c <= '9')
138
125
  number = number + c
139
126
  next
@@ -144,8 +131,7 @@ module Rufus
144
131
 
145
132
  multiplier = DURATIONS[c]
146
133
 
147
- raise "unknown time char '#{c}'" \
148
- if not multiplier
134
+ raise "unknown time char '#{c}'" unless multiplier
149
135
 
150
136
  result = result + (value * multiplier)
151
137
  end
@@ -157,29 +143,16 @@ module Rufus
157
143
  alias_method :parse_duration_string, :parse_time_string
158
144
  end
159
145
 
160
- #--
161
- # Returns true if the character c is a digit
162
- #
163
- # (probably better served by a regex)
164
- #
165
- #def Rufus.is_digit? (c)
166
- # return false if not c.kind_of?(String)
167
- # return false if c.length > 1
168
- # (c >= '0' and c <= '9')
169
- #end
170
- #++
171
-
172
146
  #
173
147
  # conversion methods between Date[Time] and Time
174
148
 
175
- #
149
+ #--
176
150
  # Ruby Cookbook 1st edition p.111
177
151
  # http://www.oreilly.com/catalog/rubyckbk/
178
152
  # a must
179
- #
153
+ #++
180
154
 
181
- #
182
- # converts a Time instance to a DateTime one
155
+ # Converts a Time instance to a DateTime one
183
156
  #
184
157
  def Rufus.to_datetime (time)
185
158
 
@@ -226,7 +199,6 @@ module Rufus
226
199
  Time.send(method, d.year, d.month, d.day, d.hour, d.min, d.sec, usec)
227
200
  end
228
201
 
229
- #
230
202
  # Turns a number of seconds into a a time string
231
203
  #
232
204
  # Rufus.to_duration_string 0 # => '0s'
@@ -284,7 +256,6 @@ module Rufus
284
256
  alias_method :to_time_string, :to_duration_string
285
257
  end
286
258
 
287
- #
288
259
  # Turns a number of seconds (integer or Float) into a hash like in :
289
260
  #
290
261
  # Rufus.to_duration_hash 0.051
@@ -331,7 +302,6 @@ module Rufus
331
302
  h
332
303
  end
333
304
 
334
- #
335
305
  # Ensures that a duration is a expressed as a Float instance.
336
306
  #
337
307
  # duration_to_f("10s")
@@ -345,6 +315,26 @@ module Rufus
345
315
  Float(s.to_s)
346
316
  end
347
317
 
318
+ #
319
+ # Ensures an 'at' value is translated to a float
320
+ # (to be compared with the float coming from time.to_f)
321
+ #
322
+ def Rufus.at_to_f (at)
323
+
324
+ # TODO : use chronic if present
325
+
326
+ at = Rufus::to_ruby_time(at) if at.is_a?(String)
327
+ at = Rufus::to_gm_time(at) if at.is_a?(DateTime)
328
+ #at = at.to_f if at.is_a?(Time)
329
+ at = at.to_f if at.respond_to?(:to_f)
330
+
331
+ raise ArgumentError.new(
332
+ "cannot determine 'at' time from : #{at.inspect}"
333
+ ) unless at.is_a?(Float)
334
+
335
+ at
336
+ end
337
+
348
338
  protected
349
339
 
350
340
  DURATIONS2M = [
@@ -0,0 +1,454 @@
1
+ #--
2
+ # Copyright (c) 2006-2009, 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/rtime'
27
+ require 'rufus/sc/cronline'
28
+ require 'rufus/sc/jobs'
29
+ require 'rufus/sc/jobqueues'
30
+
31
+
32
+ module Rufus::Scheduler
33
+
34
+ # This gem's version
35
+ #
36
+ VERSION = '2.0.0'
37
+
38
+ #
39
+ # It's OK to pass an object responding to :trigger when scheduling a job
40
+ # (instead of passing a block).
41
+ #
42
+ # This is simply a helper module. The rufus-scheduler will check if scheduled
43
+ # object quack (respond to :trigger anyway).
44
+ #
45
+ module Schedulable
46
+ def call (job)
47
+ trigger(job.params)
48
+ end
49
+ def trigger (params)
50
+ raise NotImplementedError.new('implementation is missing')
51
+ end
52
+ end
53
+
54
+ #
55
+ # For backward compatibility
56
+ #
57
+ module ::Rufus::Schedulable
58
+ extend ::Rufus::Scheduler::Schedulable
59
+ end
60
+
61
+ # Legacy from the previous version of Rufus-Scheduler.
62
+ #
63
+ # Consider all methods here as 'deprecated'.
64
+ #
65
+ module LegacyMethods
66
+
67
+ def find_jobs (tag=nil)
68
+ tag ? find_by_tag(tag) : all_jobs.values
69
+ end
70
+ def at_job_count
71
+ @jobs.select(:at).size +
72
+ @jobs.select(:in).size
73
+ end
74
+ def every_job_count
75
+ @jobs.select(:every).size
76
+ end
77
+ def cron_job_count
78
+ @cron_jobs.size
79
+ end
80
+ def pending_job_count
81
+ @jobs.size
82
+ end
83
+ def precision
84
+ @frequency
85
+ end
86
+ end
87
+
88
+ #
89
+ # The core of a rufus-scheduler. See implementations like
90
+ # Rufus::Scheduler::PlainScheduler and Rufus::Scheduler::EmScheduler for
91
+ # directly usable stuff.
92
+ #
93
+ class SchedulerCore
94
+
95
+ include LegacyMethods
96
+
97
+ # classical options hash
98
+ #
99
+ attr_reader :options
100
+
101
+ # Instantiates a Rufus::Scheduler.
102
+ #
103
+ def initialize (opts={})
104
+
105
+ @options = opts
106
+
107
+ @jobs = JobQueue.new
108
+ @cron_jobs = CronJobQueue.new
109
+
110
+ @frequency = @options[:frequency] || 0.330
111
+ end
112
+
113
+ # Instantiates and starts a new Rufus::Scheduler.
114
+ #
115
+ def self.start_new (opts={})
116
+
117
+ s = self.new(opts)
118
+ s.start
119
+ s
120
+ end
121
+
122
+ #--
123
+ # SCHEDULE METHODS
124
+ #++
125
+
126
+ # Schedules a job in a given amount of time.
127
+ #
128
+ # scheduler.in '20m' do
129
+ # puts "order ristretto"
130
+ # end
131
+ #
132
+ # will order an espresso (well sort of) in 20 minutes.
133
+ #
134
+ def in (t, s=nil, opts={}, &block)
135
+
136
+ add_job(InJob.new(self, t, combine_opts(s, opts), &block))
137
+ end
138
+ alias :schedule_in :in
139
+
140
+ # Schedules a job at a given point in time.
141
+ #
142
+ # scheduler.at 'Thu Mar 26 19:30:00 2009' do
143
+ # puts 'order pizza'
144
+ # end
145
+ #
146
+ # pizza is for Thursday at 2000 (if the shop brochure is right).
147
+ #
148
+ def at (t, s=nil, opts={}, &block)
149
+
150
+ add_job(AtJob.new(self, t, combine_opts(s, opts), &block))
151
+ end
152
+ alias :schedule_at :at
153
+
154
+ # Schedules a recurring job every t.
155
+ #
156
+ # scheduler.every '5m1w' do
157
+ # puts 'check blood pressure'
158
+ # end
159
+ #
160
+ # checking blood pressure every 5 months and 1 week.
161
+ #
162
+ def every (t, s=nil, opts={}, &block)
163
+
164
+ add_job(EveryJob.new(self, t, combine_opts(s, opts), &block))
165
+ end
166
+ alias :schedule_every :every
167
+
168
+ # Schedules a job given a cron string.
169
+ #
170
+ # scheduler.cron '0 22 * * 1-5' do
171
+ # # every day of the week at 00:22
172
+ # puts 'activate security system'
173
+ # end
174
+ #
175
+ def cron (cronstring, s=nil, opts={}, &block)
176
+
177
+ add_cron_job(CronJob.new(self, cronstring, combine_opts(s, opts), &block))
178
+ end
179
+ alias :schedule :cron
180
+
181
+ # Unschedules a job (cron or at/every/in job) given its id.
182
+ #
183
+ # Returns the job that got unscheduled.
184
+ #
185
+ def unschedule (job_id)
186
+
187
+ @jobs.unschedule(job_id) || @cron_jobs.unschedule(job_id)
188
+ end
189
+
190
+ #--
191
+ # MISC
192
+ #++
193
+
194
+ # Feel free to override this method. The default implementation simply
195
+ # outputs the error message to STDOUT
196
+ #
197
+ def handle_exception (job, exception)
198
+
199
+ if self.respond_to?(:log_exception)
200
+ #
201
+ # some kind of backward compatibility
202
+
203
+ log_exception(exception)
204
+
205
+ else
206
+
207
+ puts '=' * 80
208
+ puts "scheduler caught exception :"
209
+ puts exception
210
+ puts '=' * 80
211
+ end
212
+ end
213
+
214
+ #--
215
+ # JOB LOOKUP
216
+ #++
217
+
218
+ # Returns a map job_id => job for at/in/every jobs
219
+ #
220
+ def jobs
221
+
222
+ @jobs.to_h
223
+ end
224
+
225
+ # Returns a map job_id => job for cron jobs
226
+ #
227
+ def cron_jobs
228
+
229
+ @cron_jobs.to_h
230
+ end
231
+
232
+ # Returns a map job_id => job of all the jobs currently in the scheduler
233
+ #
234
+ def all_jobs
235
+
236
+ jobs.merge(cron_jobs)
237
+ end
238
+
239
+ # Returns a list of jobs with the given tag
240
+ #
241
+ def find_by_tag (tag)
242
+
243
+ all_jobs.values.select { |j| j.tags.include?(tag) }
244
+ end
245
+
246
+ protected
247
+
248
+ def combine_opts (schedulable, opts)
249
+
250
+ if schedulable.respond_to?(:trigger)
251
+
252
+ opts[:schedulable] = schedulable
253
+
254
+ elsif schedulable != nil
255
+
256
+ opts = schedulable.merge(opts)
257
+ end
258
+
259
+ opts
260
+ end
261
+
262
+ # The method that does the "wake up and trigger any job that should get
263
+ # triggered.
264
+ #
265
+ def step
266
+ cron_step
267
+ at_step
268
+ end
269
+
270
+ # calls every second
271
+ #
272
+ def cron_step
273
+
274
+ now = Time.now
275
+ return if now.sec == @last_cron_second
276
+ @last_cron_second = now.sec
277
+ #
278
+ # ensuring the crons are checked within 1 second (not 1.2 second)
279
+
280
+ @cron_jobs.trigger_matching_jobs(now)
281
+ end
282
+
283
+ def at_step
284
+
285
+ while job = @jobs.job_to_trigger
286
+ job.trigger
287
+ end
288
+ end
289
+
290
+ def add_job (job)
291
+
292
+ complain_if_blocking_and_timeout(job)
293
+
294
+ return if job.params[:discard_past] && Time.now.to_f >= job.at
295
+
296
+ @jobs << job
297
+
298
+ job
299
+ end
300
+
301
+ def add_cron_job (job)
302
+
303
+ complain_if_blocking_and_timeout(job)
304
+
305
+ @cron_jobs << job
306
+
307
+ job
308
+ end
309
+
310
+ # Raises an error if the job has the params :blocking and :timeout set
311
+ #
312
+ def complain_if_blocking_and_timeout (job)
313
+
314
+ raise(
315
+ ArgumentError.new('cannot set a :timeout on a :blocking job')
316
+ ) if job.params[:blocking] and job.params[:timeout]
317
+ end
318
+
319
+ # The default, plain, implementation. If 'blocking' is true, will simply
320
+ # call the block and return when the block is done.
321
+ # Else, it will call the block in a dedicated thread.
322
+ #
323
+ # TODO : clarify, the blocking here blocks the whole scheduler, while
324
+ # EmScheduler blocking triggers for the next tick. Not the same thing ...
325
+ #
326
+ def trigger_job (blocking, &block)
327
+
328
+ if blocking
329
+ block.call
330
+ else
331
+ Thread.new { block.call }
332
+ end
333
+ end
334
+ end
335
+
336
+ #--
337
+ # SCHEDULER 'IMPLEMENTATIONS'
338
+ #++
339
+
340
+ #
341
+ # A classical implementation, uses a sleep/step loop in a thread (like the
342
+ # original rufus-scheduler).
343
+ #
344
+ class PlainScheduler < SchedulerCore
345
+
346
+ def start
347
+
348
+ @thread = Thread.new do
349
+ loop do
350
+ sleep(@frequency)
351
+ self.step
352
+ end
353
+ end
354
+
355
+ @thread[:name] =
356
+ @options[:thread_name] ||
357
+ "#{self.class} - #{Rufus::Scheduler::VERSION}"
358
+ end
359
+
360
+ def stop (opts={})
361
+
362
+ @thread.exit
363
+ end
364
+
365
+ def join
366
+
367
+ @thread.join
368
+ end
369
+ end
370
+
371
+ # TODO : investigate idea
372
+ #
373
+ #class BlockingScheduler < PlainScheduler
374
+ # # use a Queue and a worker thread for the 'blocking' jobs
375
+ #end
376
+
377
+ #
378
+ # A rufus-scheduler that uses an EventMachine periodic timer instead of a
379
+ # loop.
380
+ #
381
+ class EmScheduler < SchedulerCore
382
+
383
+ def initialize (opts={})
384
+
385
+ raise LoadError.new(
386
+ 'EventMachine missing, "require \'eventmachine\'" might help'
387
+ ) unless defined?(EM)
388
+
389
+ super
390
+ end
391
+
392
+ def start
393
+
394
+ @em_thread = nil
395
+
396
+ unless EM.reactor_running?
397
+ @em_thread = Thread.new { EM.run }
398
+ while (not EM.reactor_running?)
399
+ Thread.pass
400
+ end
401
+ end
402
+
403
+ #unless EM.reactor_running?
404
+ # t = Thread.current
405
+ # @em_thread = Thread.new { EM.run { t.wakeup } }
406
+ # Thread.stop # EM will wake us up when it's ready
407
+ #end
408
+
409
+ @timer = EM::PeriodicTimer.new(@frequency) { step }
410
+ end
411
+
412
+ # Stops the scheduler.
413
+ #
414
+ # If the :stop_em option is passed and set to true, it will stop the
415
+ # EventMachine (but only if it started the EM by itself !).
416
+ #
417
+ def stop (opts={})
418
+
419
+ @timer.cancel
420
+
421
+ EM.stop if opts[:stop_em] and @em_thread
422
+ end
423
+
424
+ # Joins this scheduler. Will actually join it only if it started the
425
+ # underlying EventMachine.
426
+ #
427
+ def join
428
+
429
+ @em_thread.join if @em_thread
430
+ end
431
+
432
+ protected
433
+
434
+ # If 'blocking' is set to true, the block will get called at the
435
+ # 'next_tick'. Else the block will get called via 'defer' (own thread).
436
+ #
437
+ def trigger_job (blocking, &block)
438
+
439
+ m = blocking ? :next_tick : :defer
440
+ #
441
+ # :next_tick monopolizes the EM
442
+ # :defer executes its block in another thread
443
+
444
+ EM.send(m) { block.call }
445
+ end
446
+ end
447
+
448
+ #
449
+ # This error is thrown when the :timeout attribute triggers
450
+ #
451
+ class TimeOutError < RuntimeError
452
+ end
453
+ end
454
+