@@ -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
0 commit comments