@@ -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
+ DEFAULT_INTERVAL_TYPE = FIXED_DELAY
157
182
158
183
# Create a new TimerTask with the given task and configuration.
159
184
#
@@ -164,6 +189,9 @@ class TimerTask < RubyExecutorService
164
189
# @option opts [Boolean] :run_now Whether to run the task immediately
165
190
# upon instantiation or to wait until the first # execution_interval
166
191
# has passed (default: false)
192
+ # @options opts [Symbol] :interval_type method to calculate the interval
193
+ # between executions, can be either :fixed_rate or :fixed_delay.
194
+ # (default: :fixed_delay)
167
195
# @option opts [Executor] executor, default is `global_io_executor`
168
196
#
169
197
# @!macro deref_options
@@ -243,6 +271,10 @@ def execution_interval=(value)
243
271
end
244
272
end
245
273
274
+ # @!attribute [r] interval_type
275
+ # @return [Symbol] method to calculate the interval between executions
276
+ attr_reader :interval_type
277
+
246
278
# @!attribute [rw] timeout_interval
247
279
# @return [Fixnum] Number of seconds the task can run before it is
248
280
# considered to have failed.
@@ -265,10 +297,15 @@ def ns_initialize(opts, &task)
265
297
set_deref_options ( opts )
266
298
267
299
self . execution_interval = opts [ :execution ] || opts [ :execution_interval ] || EXECUTION_INTERVAL
300
+ if opts [ :interval_type ] && ![ FIXED_DELAY , FIXED_RATE ] . include? ( opts [ :interval_type ] )
301
+ raise ArgumentError . new ( 'interval_type must be either :fixed_delay or :fixed_rate' )
302
+ end
268
303
if opts [ :timeout ] || opts [ :timeout_interval ]
269
304
warn 'TimeTask timeouts are now ignored as these were not able to be implemented correctly'
270
305
end
306
+
271
307
@run_now = opts [ :now ] || opts [ :run_now ]
308
+ @interval_type = opts [ :interval_type ] || DEFAULT_INTERVAL_TYPE
272
309
@task = Concurrent ::SafeTaskExecutor . new ( task )
273
310
@executor = opts [ :executor ] || Concurrent . global_io_executor
274
311
@running = Concurrent ::AtomicBoolean . new ( false )
@@ -298,16 +335,27 @@ def schedule_next_task(interval = execution_interval)
298
335
# @!visibility private
299
336
def execute_task ( completion )
300
337
return nil unless @running . true?
338
+ start_time = Concurrent . monotonic_time
301
339
_success , value , reason = @task . execute ( self )
302
340
if completion . try?
303
341
self . value = value
304
- schedule_next_task
342
+ schedule_next_task ( calculate_next_interval ( start_time ) )
305
343
time = Time . now
306
344
observers . notify_observers do
307
345
[ time , self . value , reason ]
308
346
end
309
347
end
310
348
nil
311
349
end
350
+
351
+ # @!visibility private
352
+ def calculate_next_interval ( start_time )
353
+ if @interval_type == FIXED_RATE
354
+ run_time = Concurrent . monotonic_time - start_time
355
+ [ execution_interval - run_time , 0 ] . max
356
+ else # FIXED_DELAY
357
+ execution_interval
358
+ end
359
+ end
312
360
end
313
361
end
0 commit comments