From d32da653d34d8cf27e04dc7217b62ac5a9adebdd Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 25 Jul 2026 02:42:36 +0000 Subject: [PATCH] fix(geode): make multi-relay compliance test thread-safe multiRelayPoolReturnsContentFromEachRelay flaked with "expected: but was:": the SubscriptionListener wrote the per-relay results into a plain HashMap/HashSet, but each relay delivers its EVENT/EOSE on its own InProcessWebSocket scope (Dispatchers.Default) and PoolRequests dispatches the listener callbacks outside any lock. Two relays therefore call `received[relay] = ...` concurrently, and a HashMap.put racing a rehash can drop an entry, leaving a relay's value null and failing the assertion. Use ConcurrentHashMap and ConcurrentHashMap.newKeySet() for the shared collections. Reproduced within 7 runs before the fix; 80 stress runs clean after. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HeAjLDNBvGPjb5bfViU3ad --- .../com/vitorpamplona/geode/Nip01ComplianceTest.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt index 41fc1b218e..4851956a13 100644 --- a/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt +++ b/geode/src/test/kotlin/com/vitorpamplona/geode/Nip01ComplianceTest.kt @@ -39,6 +39,7 @@ import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull +import java.util.concurrent.ConcurrentHashMap import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -518,8 +519,13 @@ class Nip01ComplianceTest : RelayClientTest() { hub.getOrCreate(relayA).preload(fakeEvent(1, kind = 1, content = "from-a")) hub.getOrCreate(relayB).preload(fakeEvent(2, kind = 1, content = "from-b")) - val received = mutableMapOf() - val eosed = mutableSetOf() + // Each relay delivers its EVENT/EOSE on its own InProcessWebSocket + // scope, so these callbacks fire concurrently from two threads. The + // shared collections MUST be thread-safe — a plain HashMap loses a + // write when both threads put during a rehash, leaving a relay's + // entry null and flaking the assertions below. + val received = ConcurrentHashMap() + val eosed = ConcurrentHashMap.newKeySet() val ch = Channel(UNLIMITED) client.subscribe( "multi-1",