@@ -32,6 +32,17 @@ module Concurrent
3232 # be tested separately then passed to the `TimerTask` for scheduling and
3333 # running.
3434 #
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+ #
3546 # In some cases it may be necessary for a `TimerTask` to affect its own
3647 # execution cycle. To facilitate this, a reference to the TimerTask instance
3748 # is passed as an argument to the provided block every time the task is
@@ -74,6 +85,12 @@ module Concurrent
7485 #
7586 # #=> 'Boom!'
7687 #
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+ #
7794 # @example Last `#value` and `Dereferenceable` mixin
7895 # task = Concurrent::TimerTask.new(
7996 # dup_on_deref: true,
@@ -152,8 +169,16 @@ class TimerTask < RubyExecutorService
152169 # Default `:execution_interval` in seconds.
153170 EXECUTION_INTERVAL = 60
154171
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
157182
158183 # Create a new TimerTask with the given task and configuration.
159184 #
@@ -242,6 +267,24 @@ def execution_interval=(value)
242267 end
243268 end
244269
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+
245288 # @!attribute [rw] timeout_interval
246289 # @return [Fixnum] Number of seconds the task can run before it is
247290 # considered to have failed.
@@ -264,9 +307,11 @@ def ns_initialize(opts, &task)
264307 set_deref_options ( opts )
265308
266309 self . execution_interval = opts [ :execution ] || opts [ :execution_interval ] || EXECUTION_INTERVAL
310+ self . interval_type = opts [ :interval_type ] || INTERVAL_TYPE
267311 if opts [ :timeout ] || opts [ :timeout_interval ]
268312 warn 'TimeTask timeouts are now ignored as these were not able to be implemented correctly'
269313 end
314+
270315 @run_now = opts [ :now ] || opts [ :run_now ]
271316 @executor = Concurrent ::SafeTaskExecutor . new ( task )
272317 @running = Concurrent ::AtomicBoolean . new ( false )
@@ -296,10 +341,12 @@ def schedule_next_task(interval = execution_interval)
296341 # @!visibility private
297342 def execute_task ( completion )
298343 return nil unless @running . true?
344+ start = Concurrent . monotonic_time
299345 _success , value , reason = @executor . execute ( self )
300346 if completion . try?
301347 self . value = value
302- schedule_next_task
348+ interval = interval_type == FIXED_DELAY ? execution_interval : [ execution_interval - ( Concurrent . monotonic_time - start ) , 0 ] . max
349+ schedule_next_task ( interval )
303350 time = Time . now
304351 observers . notify_observers do
305352 [ time , self . value , reason ]
0 commit comments