rufus-scheduler 1.0.14 → 2.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.
@@ -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
+