@@ -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+ DEFAULT_INTERVAL_TYPE = FIXED_DELAY
157182
158183 # Create a new TimerTask with the given task and configuration.
159184 #
@@ -164,6 +189,9 @@ class TimerTask < RubyExecutorService
164189 # @option opts [Boolean] :run_now Whether to run the task immediately
165190 # upon instantiation or to wait until the first # execution_interval
166191 # 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)
167195 # @option opts [Executor] executor, default is `global_io_executor`
168196 #
169197 # @!macro deref_options
@@ -243,6 +271,10 @@ def execution_interval=(value)
243271 end
244272 end
245273
274+ # @!attribute [r] interval_type
275+ # @return [Symbol] method to calculate the interval between executions
276+ attr_reader :interval_type
277+
246278 # @!attribute [rw] timeout_interval
247279 # @return [Fixnum] Number of seconds the task can run before it is
248280 # considered to have failed.
@@ -265,10 +297,15 @@ def ns_initialize(opts, &task)
265297 set_deref_options ( opts )
266298
267299 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
268303 if opts [ :timeout ] || opts [ :timeout_interval ]
269304 warn 'TimeTask timeouts are now ignored as these were not able to be implemented correctly'
270305 end
306+
271307 @run_now = opts [ :now ] || opts [ :run_now ]
308+ @interval_type = opts [ :interval_type ] || DEFAULT_INTERVAL_TYPE
272309 @task = Concurrent ::SafeTaskExecutor . new ( task )
273310 @executor = opts [ :executor ] || Concurrent . global_io_executor
274311 @running = Concurrent ::AtomicBoolean . new ( false )
@@ -298,16 +335,27 @@ def schedule_next_task(interval = execution_interval)
298335 # @!visibility private
299336 def execute_task ( completion )
300337 return nil unless @running . true?
338+ start_time = Concurrent . monotonic_time
301339 _success , value , reason = @task . execute ( self )
302340 if completion . try?
303341 self . value = value
304- schedule_next_task
342+ schedule_next_task ( calculate_next_interval ( start_time ) )
305343 time = Time . now
306344 observers . notify_observers do
307345 [ time , self . value , reason ]
308346 end
309347 end
310348 nil
311349 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
312360 end
313361end
0 commit comments