rufus-scheduler 3.5.2 → 3.8.1

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