rufus-scheduler 3.6.0 → 3.7.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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +13 -0
- data/CREDITS.md +5 -0
- data/LICENSE.txt +1 -1
- data/Makefile +1 -1
- data/README.md +73 -11
- data/lib/rufus/scheduler.rb +528 -435
- data/lib/rufus/scheduler/job_array.rb +37 -47
- data/lib/rufus/scheduler/jobs_core.rb +363 -0
- data/lib/rufus/scheduler/jobs_one_time.rb +53 -0
- data/lib/rufus/scheduler/jobs_repeat.rb +333 -0
- data/lib/rufus/scheduler/locks.rb +41 -44
- data/lib/rufus/scheduler/util.rb +166 -150
- data/rufus-scheduler.gemspec +1 -2
- metadata +11 -10
- data/lib/rufus/scheduler/jobs.rb +0 -701
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
class Rufus::Scheduler::OneTimeJob < Rufus::Scheduler::Job
|
3
|
+
|
4
|
+
alias time next_time
|
5
|
+
|
6
|
+
def occurrences(time0, time1)
|
7
|
+
|
8
|
+
(time >= time0 && time <= time1) ? [ time ] : []
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def determine_id
|
14
|
+
|
15
|
+
[
|
16
|
+
self.class.name.split(':').last.downcase[0..-4],
|
17
|
+
@scheduled_at.to_f,
|
18
|
+
@next_time.to_f,
|
19
|
+
(self.object_id < 0 ? 'm' : '') + self.object_id.to_s
|
20
|
+
].map(&:to_s).join('_')
|
21
|
+
end
|
22
|
+
|
23
|
+
# There is no next_time for one time jobs, hence the false.
|
24
|
+
#
|
25
|
+
def set_next_time(trigger_time, is_post=false, now=nil)
|
26
|
+
|
27
|
+
@next_time = is_post ? nil : false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Rufus::Scheduler::AtJob < Rufus::Scheduler::OneTimeJob
|
32
|
+
|
33
|
+
def initialize(scheduler, time, opts, block)
|
34
|
+
|
35
|
+
super(scheduler, time, opts, block)
|
36
|
+
|
37
|
+
@next_time =
|
38
|
+
opts[:_t] || Rufus::Scheduler.parse_at(time, opts)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Rufus::Scheduler::InJob < Rufus::Scheduler::OneTimeJob
|
43
|
+
|
44
|
+
def initialize(scheduler, duration, opts, block)
|
45
|
+
|
46
|
+
super(scheduler, duration, opts, block)
|
47
|
+
|
48
|
+
@next_time =
|
49
|
+
@scheduled_at +
|
50
|
+
opts[:_t] || Rufus::Scheduler.parse_in(duration, opts)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,333 @@
|
|
1
|
+
|
2
|
+
class Rufus::Scheduler::RepeatJob < Rufus::Scheduler::Job
|
3
|
+
|
4
|
+
attr_reader :paused_at
|
5
|
+
|
6
|
+
attr_reader :first_at
|
7
|
+
attr_reader :last_at
|
8
|
+
attr_accessor :times
|
9
|
+
|
10
|
+
def initialize(scheduler, duration, opts, block)
|
11
|
+
|
12
|
+
super
|
13
|
+
|
14
|
+
@paused_at = nil
|
15
|
+
|
16
|
+
@times = opts[:times]
|
17
|
+
|
18
|
+
fail ArgumentError.new(
|
19
|
+
"cannot accept :times => #{@times.inspect}, not nil or an int"
|
20
|
+
) unless @times == nil || @times.is_a?(Integer)
|
21
|
+
|
22
|
+
self.first_at =
|
23
|
+
opts[:first] || opts[:first_time] ||
|
24
|
+
opts[:first_at] || opts[:first_in] ||
|
25
|
+
nil
|
26
|
+
self.last_at =
|
27
|
+
opts[:last] || opts[:last_at] || opts[:last_in]
|
28
|
+
end
|
29
|
+
|
30
|
+
FIRSTS = [ :now, :immediately, 0 ].freeze
|
31
|
+
|
32
|
+
def first_at=(first)
|
33
|
+
|
34
|
+
return (@first_at = nil) if first == nil
|
35
|
+
|
36
|
+
n0 = EoTime.now
|
37
|
+
n1 = n0 + 0.003
|
38
|
+
|
39
|
+
first = n0 if FIRSTS.include?(first)
|
40
|
+
fdur = Rufus::Scheduler.parse_duration(first, no_error: true)
|
41
|
+
|
42
|
+
@first_at = (fdur && (EoTime.now + fdur)) || EoTime.make(first)
|
43
|
+
@first_at = n1 if @first_at >= n0 && @first_at < n1
|
44
|
+
|
45
|
+
fail ArgumentError.new(
|
46
|
+
"cannot set first[_at|_in] in the past: " +
|
47
|
+
"#{first.inspect} -> #{@first_at.inspect}"
|
48
|
+
) if @first_at < n0
|
49
|
+
|
50
|
+
@first_at
|
51
|
+
end
|
52
|
+
|
53
|
+
def last_at=(last)
|
54
|
+
|
55
|
+
@last_at =
|
56
|
+
if last
|
57
|
+
ldur = Rufus::Scheduler.parse_duration(last, no_error: true)
|
58
|
+
(ldur && (EoTime.now + ldur)) || EoTime.make(last)
|
59
|
+
else
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
fail ArgumentError.new(
|
64
|
+
"cannot set last[_at|_in] in the past: " +
|
65
|
+
"#{last.inspect} -> #{@last_at.inspect}"
|
66
|
+
) if last && @last_at < EoTime.now
|
67
|
+
|
68
|
+
@last_at
|
69
|
+
end
|
70
|
+
|
71
|
+
def trigger(time)
|
72
|
+
|
73
|
+
return if @paused_at
|
74
|
+
#return set_next_time(time) if @paused_at
|
75
|
+
|
76
|
+
return (@next_time = nil) if @times && @times < 1
|
77
|
+
return (@next_time = nil) if @last_at && time >= @last_at
|
78
|
+
#
|
79
|
+
# It keeps jobs one step too much in @jobs, but it's OK
|
80
|
+
|
81
|
+
super
|
82
|
+
|
83
|
+
@times -= 1 if @times
|
84
|
+
end
|
85
|
+
|
86
|
+
def pause
|
87
|
+
|
88
|
+
@paused_at = EoTime.now
|
89
|
+
end
|
90
|
+
|
91
|
+
def resume(opts={})
|
92
|
+
|
93
|
+
@resume_discard_past = opts[:discard_past]
|
94
|
+
#p [ :@resume_discard_past, @resume_discard_past ]
|
95
|
+
@paused_at = nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def paused?
|
99
|
+
|
100
|
+
!! @paused_at
|
101
|
+
end
|
102
|
+
|
103
|
+
def determine_id
|
104
|
+
|
105
|
+
[
|
106
|
+
self.class.name.split(':').last.downcase[0..-4],
|
107
|
+
@scheduled_at.to_f,
|
108
|
+
(self.object_id < 0 ? 'm' : '') + self.object_id.to_s
|
109
|
+
].map(&:to_s).join('_')
|
110
|
+
end
|
111
|
+
|
112
|
+
def occurrences(time0, time1)
|
113
|
+
|
114
|
+
a = []
|
115
|
+
|
116
|
+
nt = @next_time
|
117
|
+
ts = @times
|
118
|
+
|
119
|
+
loop do
|
120
|
+
|
121
|
+
break if nt > time1
|
122
|
+
break if ts && ts <= 0
|
123
|
+
|
124
|
+
a << nt if nt >= time0
|
125
|
+
|
126
|
+
nt = next_time_from(nt)
|
127
|
+
ts = ts - 1 if ts
|
128
|
+
end
|
129
|
+
|
130
|
+
a
|
131
|
+
end
|
132
|
+
|
133
|
+
# Starting from now, returns the {count} next occurences
|
134
|
+
# (EtOrbi::EoTime instances) for this job.
|
135
|
+
#
|
136
|
+
# Warning, for IntervalJob, the @mean_work_time is used since
|
137
|
+
# "interval" works from the end of a job to its next trigger
|
138
|
+
# (not from one trigger to the next, as for "cron" and "every").
|
139
|
+
#
|
140
|
+
def next_times(count)
|
141
|
+
|
142
|
+
(count - 1).times.inject([ next_time ]) { |a|
|
143
|
+
a << next_time_from(a.last)
|
144
|
+
a }
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
def discard_past?
|
150
|
+
|
151
|
+
dp = @scheduler.discard_past
|
152
|
+
dp = @discard_past if @discard_past != nil
|
153
|
+
dp = @resume_discard_past if @resume_discard_past != nil
|
154
|
+
|
155
|
+
dp
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
#
|
160
|
+
# A parent class of EveryJob and IntervalJob
|
161
|
+
#
|
162
|
+
class Rufus::Scheduler::EvInJob < Rufus::Scheduler::RepeatJob
|
163
|
+
|
164
|
+
def first_at=(first)
|
165
|
+
|
166
|
+
@next_time = super
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class Rufus::Scheduler::EveryJob < Rufus::Scheduler::EvInJob
|
171
|
+
|
172
|
+
attr_reader :frequency
|
173
|
+
|
174
|
+
attr_accessor :resume_discard_past
|
175
|
+
|
176
|
+
def initialize(scheduler, duration, opts, block)
|
177
|
+
|
178
|
+
super(scheduler, duration, opts, block)
|
179
|
+
|
180
|
+
@frequency = Rufus::Scheduler.parse_in(@original)
|
181
|
+
@discard_past = opts[:discard_past]
|
182
|
+
|
183
|
+
fail ArgumentError.new(
|
184
|
+
"cannot schedule #{self.class} with a frequency " +
|
185
|
+
"of #{@frequency.inspect} (#{@original.inspect})"
|
186
|
+
) if @frequency <= 0
|
187
|
+
|
188
|
+
set_next_time(nil)
|
189
|
+
end
|
190
|
+
|
191
|
+
def check_frequency
|
192
|
+
|
193
|
+
fail ArgumentError.new(
|
194
|
+
"job frequency (#{@frequency}s) is higher than " +
|
195
|
+
"scheduler frequency (#{@scheduler.frequency}s)"
|
196
|
+
) if @frequency < @scheduler.frequency
|
197
|
+
end
|
198
|
+
|
199
|
+
def next_time_from(time)
|
200
|
+
|
201
|
+
time + @frequency
|
202
|
+
end
|
203
|
+
|
204
|
+
protected
|
205
|
+
|
206
|
+
def set_next_time(trigger_time, is_post=false, now=nil)
|
207
|
+
|
208
|
+
return if is_post
|
209
|
+
|
210
|
+
n = now || EoTime.now
|
211
|
+
|
212
|
+
return @next_time = @first_at \
|
213
|
+
if @first_at && (trigger_time == nil || @first_at > n)
|
214
|
+
|
215
|
+
dp = discard_past?
|
216
|
+
|
217
|
+
loop do
|
218
|
+
|
219
|
+
@next_time = (@next_time || n) + @frequency
|
220
|
+
|
221
|
+
break if dp == false
|
222
|
+
break if @next_time > n
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class Rufus::Scheduler::IntervalJob < Rufus::Scheduler::EvInJob
|
228
|
+
|
229
|
+
attr_reader :interval
|
230
|
+
|
231
|
+
def initialize(scheduler, interval, opts, block)
|
232
|
+
|
233
|
+
super(scheduler, interval, opts, block)
|
234
|
+
|
235
|
+
@interval = Rufus::Scheduler.parse_in(@original)
|
236
|
+
|
237
|
+
fail ArgumentError.new(
|
238
|
+
"cannot schedule #{self.class} with an interval " +
|
239
|
+
"of #{@interval.inspect} (#{@original.inspect})"
|
240
|
+
) if @interval <= 0
|
241
|
+
|
242
|
+
set_next_time(nil)
|
243
|
+
end
|
244
|
+
|
245
|
+
def next_time_from(time)
|
246
|
+
|
247
|
+
time + @mean_work_time + @interval
|
248
|
+
end
|
249
|
+
|
250
|
+
protected
|
251
|
+
|
252
|
+
def set_next_time(trigger_time, is_post=false, now=nil)
|
253
|
+
|
254
|
+
n = now || EoTime.now
|
255
|
+
|
256
|
+
@next_time =
|
257
|
+
if is_post
|
258
|
+
n + @interval
|
259
|
+
elsif trigger_time.nil?
|
260
|
+
if @first_at == nil || @first_at < n
|
261
|
+
n + @interval
|
262
|
+
else
|
263
|
+
@first_at
|
264
|
+
end
|
265
|
+
else
|
266
|
+
false
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
class Rufus::Scheduler::CronJob < Rufus::Scheduler::RepeatJob
|
272
|
+
|
273
|
+
attr_reader :cron_line
|
274
|
+
|
275
|
+
def initialize(scheduler, cronline, opts, block)
|
276
|
+
|
277
|
+
super(scheduler, cronline, opts, block)
|
278
|
+
|
279
|
+
@cron_line = opts[:_t] || ::Fugit::Cron.do_parse(cronline)
|
280
|
+
|
281
|
+
set_next_time(nil)
|
282
|
+
end
|
283
|
+
|
284
|
+
def check_frequency
|
285
|
+
|
286
|
+
return if @scheduler.frequency <= 1
|
287
|
+
#
|
288
|
+
# The minimum time delta in a cron job is 1 second, so if the
|
289
|
+
# scheduler frequency is less than that, no worries.
|
290
|
+
|
291
|
+
f = @cron_line.rough_frequency
|
292
|
+
|
293
|
+
fail ArgumentError.new(
|
294
|
+
"job frequency (min ~#{f}s) is higher than " +
|
295
|
+
"scheduler frequency (#{@scheduler.frequency}s)"
|
296
|
+
) if f < @scheduler.frequency
|
297
|
+
end
|
298
|
+
|
299
|
+
def brute_frequency
|
300
|
+
|
301
|
+
@cron_line.brute_frequency
|
302
|
+
end
|
303
|
+
|
304
|
+
def rough_frequency
|
305
|
+
|
306
|
+
@cron_line.rough_frequency
|
307
|
+
end
|
308
|
+
|
309
|
+
def next_time_from(time)
|
310
|
+
|
311
|
+
@cron_line.next_time(time)
|
312
|
+
end
|
313
|
+
|
314
|
+
protected
|
315
|
+
|
316
|
+
def set_next_time(trigger_time, is_post=false, now=nil)
|
317
|
+
|
318
|
+
return if is_post
|
319
|
+
|
320
|
+
t = trigger_time || now || EoTime.now
|
321
|
+
|
322
|
+
previous = @previous_time || @scheduled_at
|
323
|
+
t = previous if ! discard_past? && t > previous
|
324
|
+
|
325
|
+
@next_time =
|
326
|
+
if @first_at && @first_at > t
|
327
|
+
@first_at
|
328
|
+
else
|
329
|
+
@cron_line.next_time(t)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
@@ -2,71 +2,68 @@
|
|
2
2
|
require 'fileutils'
|
3
3
|
|
4
4
|
|
5
|
-
|
5
|
+
#
|
6
|
+
# A lock that can always be acquired
|
7
|
+
#
|
8
|
+
class Rufus::Scheduler::NullLock
|
6
9
|
|
10
|
+
# Locking is always successful.
|
7
11
|
#
|
8
|
-
|
9
|
-
#
|
10
|
-
class NullLock
|
11
|
-
|
12
|
-
# Locking is always successful.
|
13
|
-
#
|
14
|
-
def lock; true; end
|
12
|
+
def lock; true; end
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
def locked?; true; end
|
15
|
+
def unlock; true; end
|
16
|
+
end
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
18
|
+
#
|
19
|
+
# The standard flock mechanism, with its own class thanks to @ecin
|
20
|
+
#
|
21
|
+
class Rufus::Scheduler::FileLock
|
24
22
|
|
25
|
-
|
23
|
+
attr_reader :path
|
26
24
|
|
27
|
-
|
25
|
+
def initialize(path)
|
28
26
|
|
29
|
-
|
30
|
-
|
27
|
+
@path = path.to_s
|
28
|
+
end
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
# Locking is successful if this Ruby process can create and lock
|
31
|
+
# its lockfile (at the given path).
|
32
|
+
#
|
33
|
+
def lock
|
36
34
|
|
37
|
-
|
35
|
+
return true if locked?
|
38
36
|
|
39
|
-
|
37
|
+
@lockfile = nil
|
40
38
|
|
41
|
-
|
39
|
+
FileUtils.mkdir_p(::File.dirname(@path))
|
42
40
|
|
43
|
-
|
44
|
-
|
41
|
+
file = File.new(@path, File::RDWR | File::CREAT)
|
42
|
+
locked = file.flock(File::LOCK_NB | File::LOCK_EX)
|
45
43
|
|
46
|
-
|
44
|
+
return false unless locked
|
47
45
|
|
48
|
-
|
46
|
+
now = Time.now
|
49
47
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
48
|
+
file.print("pid: #{$$}, ")
|
49
|
+
file.print("scheduler.object_id: #{self.object_id}, ")
|
50
|
+
file.print("time: #{now}, ")
|
51
|
+
file.print("timestamp: #{now.to_f}")
|
52
|
+
file.flush
|
55
53
|
|
56
|
-
|
54
|
+
@lockfile = file
|
57
55
|
|
58
|
-
|
59
|
-
|
56
|
+
true
|
57
|
+
end
|
60
58
|
|
61
|
-
|
59
|
+
def unlock
|
62
60
|
|
63
|
-
|
64
|
-
|
61
|
+
!! (@lockfile && @lockfile.flock(File::LOCK_UN))
|
62
|
+
end
|
65
63
|
|
66
|
-
|
64
|
+
def locked?
|
67
65
|
|
68
|
-
|
69
|
-
end
|
66
|
+
!! (@lockfile && @lockfile.flock(File::LOCK_NB | File::LOCK_EX))
|
70
67
|
end
|
71
68
|
end
|
72
69
|
|