Skip to content

Commit af983a1

Browse files
committed
Allow TimerTask to be safely restarted after shutdown and avoid duplicate tasks
1 parent eae2851 commit af983a1

File tree

2 files changed

+21
-2
lines changed

2 files changed

+21
-2
lines changed

lib/concurrent-ruby/concurrent/timer_task.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require 'concurrent/concern/dereferenceable'
33
require 'concurrent/concern/observable'
44
require 'concurrent/atomic/atomic_boolean'
5+
require 'concurrent/atomic/atomic_fixnum'
56
require 'concurrent/executor/executor_service'
67
require 'concurrent/executor/ruby_executor_service'
78
require 'concurrent/executor/safe_task_executor'
@@ -236,6 +237,7 @@ def execute
236237
synchronize do
237238
if @running.false?
238239
@running.make_true
240+
@age.increment
239241
schedule_next_task(@run_now ? 0 : @execution_interval)
240242
end
241243
end
@@ -309,6 +311,7 @@ def ns_initialize(opts, &task)
309311
@task = Concurrent::SafeTaskExecutor.new(task)
310312
@executor = opts[:executor] || Concurrent.global_io_executor
311313
@running = Concurrent::AtomicBoolean.new(false)
314+
@age = Concurrent::AtomicFixnum.new(0)
312315
@value = nil
313316

314317
self.observers = Collection::CopyOnNotifyObserverSet.new
@@ -328,13 +331,15 @@ def ns_kill_execution
328331

329332
# @!visibility private
330333
def schedule_next_task(interval = execution_interval)
331-
ScheduledTask.execute(interval, executor: @executor, args: [Concurrent::Event.new], &method(:execute_task))
334+
ScheduledTask.execute(interval, executor: @executor, args: [Concurrent::Event.new, @age.value], &method(:execute_task))
332335
nil
333336
end
334337

335338
# @!visibility private
336-
def execute_task(completion)
339+
def execute_task(completion, age_when_scheduled)
337340
return nil unless @running.true?
341+
return nil unless @age.value == age_when_scheduled
342+
338343
start_time = Concurrent.monotonic_time
339344
_success, value, reason = @task.execute(self)
340345
if completion.try?

spec/concurrent/timer_task_spec.rb

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require_relative 'concern/dereferenceable_shared'
22
require_relative 'concern/observable_shared'
3+
require 'concurrent/atomic/atomic_fixnum'
34
require 'concurrent/timer_task'
45

56
module Concurrent
@@ -116,6 +117,19 @@ def trigger_observable(observable)
116117
sleep(0.1)
117118
expect(task.shutdown).to be_truthy
118119
end
120+
121+
it 'will cancel pre-shutdown task even if restarted to avoid double-runs' do
122+
counter = Concurrent::AtomicFixnum.new(0)
123+
task = TimerTask.execute(execution_interval: 0.2, run_now: true) { counter.increment }
124+
sleep 0.05
125+
expect(counter.value).to eq 1
126+
127+
task.shutdown
128+
task.execute
129+
130+
sleep 0.25
131+
expect(counter.value).to eq 3
132+
end
119133
end
120134
end
121135

0 commit comments

Comments
 (0)