rufus-scheduler 3.5.2 → 3.8.1

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.
@@ -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
+