fix(nests): publish handles list under AtomicInteger barrier in speaker test

ScriptedSpeaker mutated `handles` AFTER incrementing `_startCount`,
so a reader that polled startCount on a different thread (the test
runs on the runBlocking thread while the broadcast pump runs on
Dispatchers.Default) could see startCount==1 via the volatile
AtomicInteger read before the subsequent list write became visible,
then fail `assertEquals(1, first.handles.size)` with a stale 0.

Also `mutableListOf` itself is not thread-safe — concurrent reads
during a write are undefined.

Reorder so the list append happens BEFORE the increment (the
volatile write now publishes the list mutation under the same
happens-before edge tests already rely on for startCount), and
swap the backing store for CopyOnWriteArrayList so concurrent
reads of `handles[0]` are well-defined.

Observed as a flake on Ubuntu CI; passes locally.
This commit is contained in:
Claude
2026-04-30 14:34:53 +00:00
parent 79e4a841e7
commit dd9a530305
@@ -124,12 +124,27 @@ class ReconnectingNestsSpeakerTest {
private val _startCount = AtomicInteger(0)
val startCount: Int get() = _startCount.get()
val handles = mutableListOf<ScriptedBroadcastHandle>()
// CopyOnWriteArrayList — the broadcast pump runs on
// Dispatchers.Default while tests read `handles[0]` from the
// runBlocking thread. A plain `mutableListOf` is neither
// thread-safe nor publishes its writes; observed flake on
// Ubuntu CI where the test thread sees startCount==1 (volatile
// read on AtomicInteger) before the subsequent list mutation
// becomes visible. Using a thread-safe list AND ordering the
// list mutation BEFORE the AtomicInteger increment ensures
// that any reader who sees startCount>=N also sees N entries
// already published into `handles`.
val handles: MutableList<ScriptedBroadcastHandle> =
java.util.concurrent.CopyOnWriteArrayList()
override suspend fun startBroadcasting(): BroadcastHandle {
_startCount.incrementAndGet()
val handle = ScriptedBroadcastHandle()
handles += handle
// Increment AFTER appending to `handles` so the volatile
// write on AtomicInteger publishes the list mutation —
// tests poll on startCount and then read handles[index].
_startCount.incrementAndGet()
// Mirror the production speaker's contract: transition
// Connected → Broadcasting on startBroadcasting.
val current = mutableState.value