@@ -32,6 +32,17 @@ module Concurrent
32
32
# be tested separately then passed to the `TimerTask` for scheduling and
33
33
# running.
34
34
#
35
+ # A `TimerTask` supports two different types of interval calculations.
36
+ # A fixed delay will always wait the same amount of time between the
37
+ # completion of one task and the start of the next. A fixed rate will
38
+ # attempt to maintain a constant rate of execution regardless of the
39
+ # duration of the task. For example, if a fixed rate task is scheduled
40
+ # to run every 60 seconds but the task itself takes 10 seconds to
41
+ # complete, the next task will be scheduled to run 50 seconds after
42
+ # the start of the previous task. If the task takes 70 seconds to
43
+ # complete, the next task will be start immediately after the previous
44
+ # task completes. Tasks will not be executed concurrently.
45
+ #
35
46
# In some cases it may be necessary for a `TimerTask` to affect its own
36
47
# execution cycle. To facilitate this, a reference to the TimerTask instance
37
48
# is passed as an argument to the provided block every time the task is
@@ -74,6 +85,12 @@ module Concurrent
74
85
#
75
86
# #=> 'Boom!'
76
87
#
88
+ # @example Configuring `:interval_type` with either :fixed_delay or :fixed_rate, default is :fixed_delay
89
+ # task = Concurrent::TimerTask.new(execution_interval: 5, interval_type: :fixed_rate) do
90
+ # puts 'Boom!'
91
+ # end
92
+ # task.interval_type #=> :fixed_rate
93
+ #
77
94
# @example Last `#value` and `Dereferenceable` mixin
78
95
# task = Concurrent::TimerTask.new(
79
96
# dup_on_deref: true,
@@ -152,8 +169,16 @@ class TimerTask < RubyExecutorService
152
169
# Default `:execution_interval` in seconds.
153
170
EXECUTION_INTERVAL = 60
154
171
155
- # Default `:timeout_interval` in seconds.
156
- TIMEOUT_INTERVAL = 30
172
+ # Maintain the interval between the end of one execution and the start of the next execution.
173
+ FIXED_DELAY = :fixed_delay
174
+
175
+ # Maintain the interval between the start of one execution and the start of the next.
176
+ # If execution time exceeds the interval, the next execution will start immediately
177
+ # after the previous execution finishes. Executions will not run concurrently.
178
+ FIXED_RATE = :fixed_rate
179
+
180
+ # Default `:interval_type`
181
+ INTERVAL_TYPE = FIXED_DELAY
157
182
158
183
# Create a new TimerTask with the given task and configuration.
159
184
#
@@ -242,6 +267,24 @@ def execution_interval=(value)
242
267
end
243
268
end
244
269
270
+ # @!attribute [rw] interval_type
271
+ # @return [Symbol] method to calculate the interval between executions, can be either
272
+ # :fixed_rate or :fixed_delay; default to :fixed_delay.
273
+ def interval_type
274
+ synchronize { @interval_type }
275
+ end
276
+
277
+ # @!attribute [rw] interval_type
278
+ # @return [Symbol] method to calculate the interval between executions, can be either
279
+ # :fixed_rate or :fixed_delay; default to :fixed_delay.
280
+ def interval_type = ( value )
281
+ if [ FIXED_DELAY , FIXED_RATE ] . include? ( value )
282
+ synchronize { @interval_type = value }
283
+ else
284
+ raise ArgumentError . new ( 'must be either :fixed_delay or :fixed_rate' )
285
+ end
286
+ end
287
+
245
288
# @!attribute [rw] timeout_interval
246
289
# @return [Fixnum] Number of seconds the task can run before it is
247
290
# considered to have failed.
@@ -264,6 +307,7 @@ def ns_initialize(opts, &task)
264
307
set_deref_options ( opts )
265
308
266
309
self . execution_interval = opts [ :execution ] || opts [ :execution_interval ] || EXECUTION_INTERVAL
310
+ self . interval_type = opts [ :interval_type ] || INTERVAL_TYPE
267
311
if opts [ :timeout ] || opts [ :timeout_interval ]
268
312
warn 'TimeTask timeouts are now ignored as these were not able to be implemented correctly'
269
313
end
@@ -296,10 +340,12 @@ def schedule_next_task(interval = execution_interval)
296
340
# @!visibility private
297
341
def execute_task ( completion )
298
342
return nil unless @running . true?
343
+ start = Concurrent . monotonic_time
299
344
_success , value , reason = @executor . execute ( self )
300
345
if completion . try?
301
346
self . value = value
302
- schedule_next_task
347
+ interval = interval_type == FIXED_DELAY ? execution_interval : [ execution_interval - ( Concurrent . monotonic_time - start ) , 0 ] . max
348
+ schedule_next_task ( interval )
303
349
time = Time . now
304
350
observers . notify_observers do
305
351
[ time , self . value , reason ]
0 commit comments