Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions lib/concurrent-ruby/concurrent/scheduled_task.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def initialize(delay, opts = {}, &task)
@args = get_arguments_from(opts)
@delay = delay.to_f
@task = task
@time = nil
self.schedule_time = nil
@executor = Options.executor_from_options(opts) || Concurrent.global_io_executor
self.observers = Collection::CopyOnNotifyObserverSet.new
end
Expand All @@ -203,9 +203,8 @@ def initial_delay
# The monotonic time at which the the task is scheduled to be executed.
#
# @return [Float] the schedule time or nil if `unscheduled`
def schedule_time
synchronize { @time }
end
attr_volatile :schedule_time # read by TimerSet under its own lock, so it must not take the task lock
private :schedule_time=

# Comparator which orders by schedule time.
#
Expand Down Expand Up @@ -311,7 +310,7 @@ def process_task
# @!visibility private
def ns_schedule(delay)
@delay = delay
@time = Concurrent.monotonic_time + @delay
self.schedule_time = Concurrent.monotonic_time + @delay
@parent.send(:post_task, self)
end

Expand Down
26 changes: 26 additions & 0 deletions spec/concurrent/executor/timer_set_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,32 @@ module Concurrent
expect(success).to be false
expect(task.schedule_time).to be_within(0.01).of(expected)
end

it 'does not deadlock when the timer reads the schedule time during rescheduling' do
task = subject.post(10){ nil }
task_locked = Concurrent::CountDownLatch.new
timer_locked = Concurrent::CountDownLatch.new

rescheduler = in_thread do
task.send(:synchronize) do
task_locked.count_down
timer_locked.wait(2)
task.reschedule(20)
end
end
reader = in_thread do
task_locked.wait(2)
subject.send(:synchronize) do
timer_locked.count_down
task.schedule_time
end
end

rescheduled = rescheduler.join(2)
[rescheduler, reader].each(&:kill) # cleanup, so the timer set can shut down after a deadlock
expect(rescheduled).not_to be_nil
expect(rescheduler.value).to be true
end
end

context 'task resetting' do
Expand Down
Loading