Skip to content

Commit 6e37e06

Browse files
joshuay03eregon
authored andcommitted
Fix AtomicReference#update livelock when stored value is Float::NAN
The numeric `compare_and_set` wrapper checks `old == old_value` before delegating to `_compare_and_set`. With `Float::NAN` that check is always false (`NaN != NaN`), so `#update` retries indefinitely. Handle expected NaN explicitly so `#update` can terminate.
1 parent 2825cfa commit 6e37e06

2 files changed

Lines changed: 28 additions & 1 deletion

File tree

lib/concurrent-ruby/concurrent/atomic_reference/numeric_cas_wrapper.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ module AtomicNumericCompareAndSetWrapper
99
# @!macro atomic_reference_method_compare_and_set
1010
def compare_and_set(old_value, new_value)
1111
if old_value.kind_of? Numeric
12+
# NaN is never == to itself; match it explicitly so #update can terminate.
13+
expected_nan = old_value.respond_to?(:nan?) && old_value.nan?
1214
while true
1315
old = get
1416

1517
return false unless old.kind_of? Numeric
1618

17-
return false unless old == old_value
19+
if expected_nan
20+
return false unless old.respond_to?(:nan?) && old.nan?
21+
else
22+
return false unless old == old_value
23+
end
1824

1925
result = _compare_and_set(old, new_value)
2026
return result if result

spec/concurrent/atomic/atomic_reference_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'concurrent/atomic/atomic_reference'
2+
require 'timeout'
23

34
RSpec.shared_examples :atomic_reference do
45

@@ -80,6 +81,16 @@
8081
expect(tries).to eq 2
8182
end
8283

84+
specify :test_update_nan do
85+
atomic = described_class.new(Float::NAN)
86+
tries = 0
87+
result = Timeout.timeout(2) { atomic.update { |_| tries += 1; 0.0 } }
88+
89+
expect(result).to eq 0.0
90+
expect(atomic.value).to eq 0.0
91+
expect(tries).to eq 1
92+
end
93+
8394
specify :test_numeric_cas do
8495
atomic = described_class.new(0)
8596

@@ -143,6 +154,16 @@
143154
atomic.set(Complex(1, 2))
144155
expect(atomic.compare_and_set(Complex(1, 2), 0)).to be_truthy, "CAS failed for #{Complex(1, 2)} => 0"
145156
end
157+
158+
specify :test_numeric_cas_nan do
159+
atomic = described_class.new(Float::NAN)
160+
expect(atomic.compare_and_set(Float::NAN, 0.0)).to be_truthy, "CAS should swap NaN for 0.0"
161+
expect(atomic.value).to eq 0.0
162+
163+
atomic.set(1.0)
164+
expect(atomic.compare_and_set(Float::NAN, 0.0)).to be_falsey, "CAS should not swap when current is not NaN"
165+
expect(atomic.value).to eq 1.0
166+
end
146167
end
147168

148169
module Concurrent

0 commit comments

Comments
 (0)