Skip to content

Commit 716872e

Browse files
authored
Fix race condition in Remote::Component::Barrier causing flaky boot tags (#5545)
1 parent 8bcd65c commit 716872e

3 files changed

Lines changed: 47 additions & 19 deletions

File tree

lib/datadog/core/remote/component.rb

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,44 +105,61 @@ def after_fork
105105
class Barrier
106106
def initialize(timeout = nil)
107107
@once = false
108+
@waited = false
108109
@timeout = timeout
109110

110111
@mutex = Mutex.new
111112
@condition = ConditionVariable.new
112113
end
113114

114115
# Wait for first lift to happen, otherwise don't wait
116+
#
117+
# Returns:
118+
# - :lift if the barrier was lifted (worker completed a cycle)
119+
# - :timeout if the wait timed out before the barrier was lifted
120+
# - :pass if wait_once was already called previously
121+
#
122+
# Uses a separate @waited flag to distinguish "already waited" (:pass)
123+
# from "worker lifted before we could wait" (:lift). Without this,
124+
# a race between Worker#start and wait_once can cause the first call
125+
# to return :pass if the worker completes before wait_once runs.
115126
def wait_once(timeout = nil)
116-
# TTAS (Test and Test-And-Set) optimisation
117-
# Since @once only ever goes from false to true, this is semantically valid
118-
return :pass if @once
119-
120-
begin
121-
@mutex.lock
127+
# TTAS (Test and Test-And-Set) optimisation for subsequent calls.
128+
# @waited is only set inside the mutex and only transitions false -> true,
129+
# so an unsynchronized read is safe: a stale `false` just falls through
130+
# to the synchronized path which re-checks.
131+
return :pass if @waited
122132

123-
return :pass if @once
124-
125-
timeout ||= @timeout
133+
@mutex.synchronize do
134+
return :pass if @waited
126135

127-
# - starting with Ruby 3.2, ConditionVariable#wait returns nil on
128-
# timeout and an integer otherwise
129-
# - before Ruby 3.2, ConditionVariable returns itself
130-
# so we have to rely on @once having been set
131-
if RUBY_VERSION >= '3.2'
132-
lifted = @condition.wait(@mutex, timeout)
136+
if @once
137+
# Worker lifted the barrier before we could wait.
138+
# This is still the first call, so return :lift not :pass.
139+
lifted = true
133140
else
134-
@condition.wait(@mutex, timeout)
135-
lifted = @once
141+
timeout ||= @timeout
142+
143+
# - starting with Ruby 3.2, ConditionVariable#wait returns nil on
144+
# timeout and an integer otherwise
145+
# - before Ruby 3.2, ConditionVariable returns itself
146+
# so we have to rely on @once having been set
147+
if RUBY_VERSION >= '3.2'
148+
lifted = @condition.wait(@mutex, timeout)
149+
else
150+
@condition.wait(@mutex, timeout)
151+
lifted = @once
152+
end
136153
end
137154

155+
@waited = true
156+
138157
if lifted
139158
:lift
140159
else
141160
@once = true
142161
:timeout
143162
end
144-
ensure
145-
@mutex.unlock
146163
end
147164
end
148165

sig/datadog/core/remote/component.rbs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module Datadog
4141
type timeout_s = ::Integer | ::Float
4242

4343
@once: bool
44+
@waited: bool
4445
@timeout: timeout_s?
4546

4647
@mutex: ::Thread::Mutex

spec/datadog/core/remote/component_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,5 +430,15 @@
430430
expect(record).to eq [:one, :two, :three]
431431
end
432432
end
433+
434+
context 'when lift happens before wait_once' do
435+
it 'returns :lift on first call, not :pass' do
436+
# Simulate the race: worker lifts barrier before wait_once is called
437+
barrier.lift
438+
439+
expect(barrier.wait_once).to eq :lift
440+
expect(barrier.wait_once).to eq :pass
441+
end
442+
end
433443
end
434444
end

0 commit comments

Comments
 (0)