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
@@ -1,72 +1,62 @@
|
|
1
1
|
|
2
|
-
|
2
|
+
#
|
3
|
+
# The array rufus-scheduler uses to keep jobs in order (next to trigger
|
4
|
+
# first).
|
5
|
+
#
|
6
|
+
class Rufus::Scheduler::JobArray
|
3
7
|
|
4
|
-
|
8
|
+
def initialize
|
5
9
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
class JobArray
|
11
|
-
|
12
|
-
def initialize
|
13
|
-
|
14
|
-
@mutex = Mutex.new
|
15
|
-
@array = []
|
16
|
-
end
|
17
|
-
|
18
|
-
def push(job)
|
10
|
+
@mutex = Mutex.new
|
11
|
+
@array = []
|
12
|
+
end
|
19
13
|
|
20
|
-
|
14
|
+
def push(job)
|
21
15
|
|
22
|
-
|
23
|
-
end
|
16
|
+
@mutex.synchronize { @array << job unless @array.index(job) }
|
24
17
|
|
25
|
-
|
18
|
+
self
|
19
|
+
end
|
26
20
|
|
27
|
-
|
28
|
-
end
|
21
|
+
def size
|
29
22
|
|
30
|
-
|
23
|
+
@array.size
|
24
|
+
end
|
31
25
|
|
32
|
-
|
26
|
+
def each(now, &block)
|
33
27
|
|
34
|
-
|
28
|
+
to_a.sort_by do |job|
|
35
29
|
|
36
|
-
|
30
|
+
job.next_time || (now + 1)
|
37
31
|
|
38
|
-
|
39
|
-
break if ( ! nt) || (nt > now)
|
32
|
+
end.each do |job|
|
40
33
|
|
41
|
-
|
42
|
-
|
43
|
-
end
|
34
|
+
nt = job.next_time
|
35
|
+
break if ( ! nt) || (nt > now)
|
44
36
|
|
45
|
-
|
37
|
+
block.call(job)
|
38
|
+
end
|
39
|
+
end
|
46
40
|
|
47
|
-
|
41
|
+
def delete_unscheduled
|
48
42
|
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
@mutex.synchronize {
|
44
|
+
@array.delete_if { |j| j.next_time.nil? || j.unscheduled_at } }
|
45
|
+
end
|
52
46
|
|
53
|
-
|
47
|
+
def to_a
|
54
48
|
|
55
|
-
|
56
|
-
|
49
|
+
@mutex.synchronize { @array.dup }
|
50
|
+
end
|
57
51
|
|
58
|
-
|
52
|
+
def [](job_id)
|
59
53
|
|
60
|
-
|
61
|
-
|
54
|
+
@mutex.synchronize { @array.find { |j| j.job_id == job_id } }
|
55
|
+
end
|
62
56
|
|
63
|
-
|
64
|
-
#
|
65
|
-
def array
|
57
|
+
def unschedule_all
|
66
58
|
|
67
|
-
|
68
|
-
end
|
69
|
-
end
|
59
|
+
@array.each(&:unschedule)
|
70
60
|
end
|
71
61
|
end
|
72
62
|
|
@@ -0,0 +1,363 @@
|
|
1
|
+
|
2
|
+
class Rufus::Scheduler::Job
|
3
|
+
|
4
|
+
EoTime = ::EtOrbi::EoTime
|
5
|
+
|
6
|
+
#
|
7
|
+
# Used by Job#kill
|
8
|
+
#
|
9
|
+
class KillSignal < StandardError; end
|
10
|
+
|
11
|
+
attr_reader :id
|
12
|
+
attr_reader :opts
|
13
|
+
attr_reader :original
|
14
|
+
attr_reader :scheduled_at
|
15
|
+
attr_reader :last_time
|
16
|
+
attr_reader :unscheduled_at
|
17
|
+
attr_reader :tags
|
18
|
+
attr_reader :locals
|
19
|
+
attr_reader :count
|
20
|
+
attr_reader :last_work_time
|
21
|
+
attr_reader :mean_work_time
|
22
|
+
|
23
|
+
attr_accessor :name
|
24
|
+
|
25
|
+
# next trigger time
|
26
|
+
#
|
27
|
+
attr_accessor :next_time
|
28
|
+
|
29
|
+
# previous "next trigger time"
|
30
|
+
#
|
31
|
+
attr_accessor :previous_time
|
32
|
+
|
33
|
+
# anything with a #call(job[, timet]) method,
|
34
|
+
# what gets actually triggered
|
35
|
+
#
|
36
|
+
attr_reader :callable
|
37
|
+
|
38
|
+
# a reference to the instance whose call method is the @callable
|
39
|
+
#
|
40
|
+
attr_reader :handler
|
41
|
+
|
42
|
+
# Default, core, implementation has no effect. Repeat jobs do override it.
|
43
|
+
#
|
44
|
+
def resume_discard_past=(v); end
|
45
|
+
|
46
|
+
def initialize(scheduler, original, opts, block)
|
47
|
+
|
48
|
+
@scheduler = scheduler
|
49
|
+
@original = original
|
50
|
+
@opts = opts
|
51
|
+
|
52
|
+
@handler = block
|
53
|
+
|
54
|
+
@callable =
|
55
|
+
if block.respond_to?(:arity)
|
56
|
+
block
|
57
|
+
elsif block.respond_to?(:call)
|
58
|
+
block.method(:call)
|
59
|
+
elsif block.is_a?(Class)
|
60
|
+
@handler = block.new
|
61
|
+
@handler.method(:call) rescue nil
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
@scheduled_at = EoTime.now
|
67
|
+
@unscheduled_at = nil
|
68
|
+
@last_time = nil
|
69
|
+
|
70
|
+
@locals = opts[:locals] || opts[:l] || {}
|
71
|
+
@local_mutex = Mutex.new
|
72
|
+
|
73
|
+
@id = determine_id
|
74
|
+
@name = opts[:name] || opts[:n]
|
75
|
+
|
76
|
+
fail(
|
77
|
+
ArgumentError,
|
78
|
+
'missing block or callable to schedule',
|
79
|
+
caller[2..-1]
|
80
|
+
) unless @callable
|
81
|
+
|
82
|
+
@tags = Array(opts[:tag] || opts[:tags]).collect { |t| t.to_s }
|
83
|
+
|
84
|
+
@count = 0
|
85
|
+
@last_work_time = 0.0
|
86
|
+
@mean_work_time = 0.0
|
87
|
+
|
88
|
+
# tidy up options
|
89
|
+
|
90
|
+
if @opts[:allow_overlap] == false || @opts[:allow_overlapping] == false
|
91
|
+
@opts[:overlap] = false
|
92
|
+
end
|
93
|
+
if m = @opts[:mutex]
|
94
|
+
@opts[:mutex] = Array(m)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
alias job_id id
|
99
|
+
|
100
|
+
# Will fail with an ArgumentError if the job frequency is higher than
|
101
|
+
# the scheduler frequency.
|
102
|
+
#
|
103
|
+
def check_frequency
|
104
|
+
|
105
|
+
# this parent implementation never fails
|
106
|
+
end
|
107
|
+
|
108
|
+
def trigger(time)
|
109
|
+
|
110
|
+
@previous_time = @next_time
|
111
|
+
set_next_time(time)
|
112
|
+
|
113
|
+
do_trigger(time)
|
114
|
+
end
|
115
|
+
|
116
|
+
# Trigger the job right now, off of its schedule.
|
117
|
+
#
|
118
|
+
# Done in collaboration with Piavka in
|
119
|
+
# https://github.com/jmettraux/rufus-scheduler/issues/214
|
120
|
+
#
|
121
|
+
def trigger_off_schedule(time=EoTime.now)
|
122
|
+
|
123
|
+
do_trigger(time)
|
124
|
+
end
|
125
|
+
|
126
|
+
def unschedule
|
127
|
+
|
128
|
+
@unscheduled_at = EoTime.now
|
129
|
+
end
|
130
|
+
|
131
|
+
def threads
|
132
|
+
|
133
|
+
Thread.list.select { |t| t[:rufus_scheduler_job] == self }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Kills all the threads this Job currently has going on.
|
137
|
+
#
|
138
|
+
def kill
|
139
|
+
|
140
|
+
threads.each { |t| t.raise(KillSignal) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def running?
|
144
|
+
|
145
|
+
threads.any?
|
146
|
+
end
|
147
|
+
|
148
|
+
def scheduled?
|
149
|
+
|
150
|
+
@scheduler.scheduled?(self)
|
151
|
+
end
|
152
|
+
|
153
|
+
def []=(key, value)
|
154
|
+
|
155
|
+
@local_mutex.synchronize { @locals[key] = value }
|
156
|
+
end
|
157
|
+
|
158
|
+
def [](key)
|
159
|
+
|
160
|
+
@local_mutex.synchronize { @locals[key] }
|
161
|
+
end
|
162
|
+
|
163
|
+
def has_key?(key)
|
164
|
+
|
165
|
+
@local_mutex.synchronize { @locals.has_key?(key) }
|
166
|
+
end
|
167
|
+
alias key? has_key?
|
168
|
+
|
169
|
+
def keys; @local_mutex.synchronize { @locals.keys }; end
|
170
|
+
def values; @local_mutex.synchronize { @locals.values }; end
|
171
|
+
def entries; @local_mutex.synchronize { @locals.entries }; end
|
172
|
+
|
173
|
+
#def hash
|
174
|
+
# self.object_id
|
175
|
+
#end
|
176
|
+
#def eql?(o)
|
177
|
+
# o.class == self.class && o.hash == self.hash
|
178
|
+
#end
|
179
|
+
#
|
180
|
+
# might be necessary at some point
|
181
|
+
|
182
|
+
def next_times(count)
|
183
|
+
|
184
|
+
next_time ? [ next_time ] : []
|
185
|
+
end
|
186
|
+
|
187
|
+
# Calls the callable (usually a block) wrapped in this Job instance.
|
188
|
+
#
|
189
|
+
# Warning: error rescueing is the responsibity of the caller.
|
190
|
+
#
|
191
|
+
def call(do_rescue=false)
|
192
|
+
|
193
|
+
do_call(EoTime.now, do_rescue)
|
194
|
+
end
|
195
|
+
|
196
|
+
protected
|
197
|
+
|
198
|
+
def callback(meth, time)
|
199
|
+
|
200
|
+
return true unless @scheduler.respond_to?(meth)
|
201
|
+
|
202
|
+
arity = @scheduler.method(meth).arity
|
203
|
+
args = [ self, time ][0, (arity < 0 ? 2 : arity)]
|
204
|
+
|
205
|
+
@scheduler.send(meth, *args)
|
206
|
+
end
|
207
|
+
|
208
|
+
def compute_timeout
|
209
|
+
|
210
|
+
if to = @opts[:timeout]
|
211
|
+
Rufus::Scheduler.parse(to)
|
212
|
+
else
|
213
|
+
nil
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def mutex(m)
|
218
|
+
|
219
|
+
m.is_a?(Mutex) ? m : (@scheduler.mutexes[m.to_s] ||= Mutex.new)
|
220
|
+
end
|
221
|
+
|
222
|
+
def do_call(time, do_rescue)
|
223
|
+
|
224
|
+
args = [ self, time ][0, @callable.arity]
|
225
|
+
|
226
|
+
@scheduler.around_trigger(self) do
|
227
|
+
@callable.call(*args)
|
228
|
+
end
|
229
|
+
|
230
|
+
rescue StandardError => se
|
231
|
+
|
232
|
+
fail se unless do_rescue
|
233
|
+
|
234
|
+
return if se.is_a?(KillSignal) # discard
|
235
|
+
|
236
|
+
@scheduler.on_error(self, se)
|
237
|
+
|
238
|
+
# exceptions above StandardError do pass through
|
239
|
+
end
|
240
|
+
|
241
|
+
def do_trigger(time)
|
242
|
+
|
243
|
+
return if (
|
244
|
+
opts[:overlap] == false &&
|
245
|
+
running?
|
246
|
+
)
|
247
|
+
return if (
|
248
|
+
callback(:confirm_lock, time) &&
|
249
|
+
callback(:on_pre_trigger, time)
|
250
|
+
) == false
|
251
|
+
|
252
|
+
@count += 1
|
253
|
+
|
254
|
+
if opts[:blocking]
|
255
|
+
trigger_now(time)
|
256
|
+
else
|
257
|
+
trigger_queue(time)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def trigger_now(time)
|
262
|
+
|
263
|
+
ct = Thread.current
|
264
|
+
|
265
|
+
t = EoTime.now
|
266
|
+
# if there are mutexes, t might be really bigger than time
|
267
|
+
|
268
|
+
ct[:rufus_scheduler_job] = self
|
269
|
+
ct[:rufus_scheduler_time] = t
|
270
|
+
ct[:rufus_scheduler_timeout] = compute_timeout
|
271
|
+
|
272
|
+
@last_time = t
|
273
|
+
|
274
|
+
do_call(time, true)
|
275
|
+
|
276
|
+
ensure
|
277
|
+
|
278
|
+
@last_work_time =
|
279
|
+
EoTime.now - ct[:rufus_scheduler_time]
|
280
|
+
@mean_work_time =
|
281
|
+
((@count - 1) * @mean_work_time + @last_work_time) / @count
|
282
|
+
|
283
|
+
post_trigger(time)
|
284
|
+
|
285
|
+
ct[:rufus_scheduler_job] = nil
|
286
|
+
ct[:rufus_scheduler_time] = nil
|
287
|
+
ct[:rufus_scheduler_timeout] = nil
|
288
|
+
end
|
289
|
+
|
290
|
+
def post_trigger(time)
|
291
|
+
|
292
|
+
set_next_time(time, true)
|
293
|
+
# except IntervalJob instances, jobs will ignore this call
|
294
|
+
|
295
|
+
callback(:on_post_trigger, time)
|
296
|
+
end
|
297
|
+
|
298
|
+
def start_work_thread
|
299
|
+
|
300
|
+
thread =
|
301
|
+
Thread.new do
|
302
|
+
|
303
|
+
ct = Thread.current
|
304
|
+
|
305
|
+
ct[:rufus_scheduler_job] = true
|
306
|
+
# indicates that the thread is going to be assigned immediately
|
307
|
+
|
308
|
+
ct[@scheduler.thread_key] = true
|
309
|
+
ct[:rufus_scheduler_work_thread] = true
|
310
|
+
|
311
|
+
loop do
|
312
|
+
|
313
|
+
break if @scheduler.started_at == nil
|
314
|
+
|
315
|
+
job, time = @scheduler.work_queue.pop
|
316
|
+
|
317
|
+
break if job == :shutdown
|
318
|
+
break if @scheduler.started_at == nil
|
319
|
+
|
320
|
+
next if job.unscheduled_at
|
321
|
+
|
322
|
+
begin
|
323
|
+
|
324
|
+
(job.opts[:mutex] || []).reduce(
|
325
|
+
lambda { job.trigger_now(time) }
|
326
|
+
) do |b, m|
|
327
|
+
lambda { mutex(m).synchronize { b.call } }
|
328
|
+
end.call
|
329
|
+
|
330
|
+
rescue KillSignal
|
331
|
+
|
332
|
+
# simply go on looping
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
thread[@scheduler.thread_key] = true
|
338
|
+
thread[:rufus_scheduler_work_thread] = true
|
339
|
+
#
|
340
|
+
# same as above (in the thead block),
|
341
|
+
# but since it has to be done as quickly as possible.
|
342
|
+
# So, whoever is running first (scheduler thread vs job thread)
|
343
|
+
# sets this information
|
344
|
+
|
345
|
+
thread
|
346
|
+
end
|
347
|
+
|
348
|
+
def trigger_queue(time)
|
349
|
+
|
350
|
+
threads = @scheduler.work_threads
|
351
|
+
|
352
|
+
vac = threads.select { |t| t[:rufus_scheduler_job] == nil }.size
|
353
|
+
que = @scheduler.work_queue.size
|
354
|
+
|
355
|
+
cur = threads.size
|
356
|
+
max = @scheduler.max_work_threads
|
357
|
+
|
358
|
+
start_work_thread if vac - que < 1 && cur < max
|
359
|
+
|
360
|
+
@scheduler.work_queue << [ self, time ]
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|