fix(quartz): close HashSet race in LiveEventStore.query dedupe
The previous design wrapped a mutable HashSet in an AtomicReference, which makes the *reference* thread-safe but not the set's internals. The historical-replay closure ran `seenIds.load()?.add(event.id)` from the subscriber's coroutine while `deliver` ran `seen.contains(event.id)` from the publisher's coroutine — concurrent add/contains can corrupt buckets or throw CME. The pre-index implementation didn't have this race because both operations ran on the same Flow collector coroutine. Index-driven dispatch put them on different coroutines. Switch to AtomicReference<Set<String>?> over an immutable Set with a CAS-loop on add. Reads in `deliver` always see a fully-constructed snapshot. Single-writer in steady state so the CAS typically succeeds first try; the loop only matters across the `seenIds.store(null)` post-EOSE handoff. Also: - hoist `NostrSignerSync(keys[target])` out of the per-iteration loop in LoadBenchmark.fanoutScaling. - add FilterIndex tests for `tagsAll` selection, multi-char tag fall-through, and re-register key-union semantics.
This commit is contained in:
@@ -500,6 +500,7 @@ class LoadBenchmark {
|
||||
// FilterIndex's byAuthor bucket.
|
||||
val keys = List(subs) { KeyPair() }
|
||||
val pubkeys = keys.map { it.pubKey.toHexKey() }
|
||||
val signers = keys.map { NostrSignerSync(it) }
|
||||
|
||||
// Per-subscriber arrival timestamp. We
|
||||
// overwrite per round; the publish path waits
|
||||
@@ -579,8 +580,7 @@ class LoadBenchmark {
|
||||
val start = System.nanoTime()
|
||||
for (i in 0 until totalEvents) {
|
||||
val target = i % subs
|
||||
val signer = NostrSignerSync(keys[target])
|
||||
val event = signer.sign(TextNoteEvent.build("scale-$i"))
|
||||
val event = signers[target].sign(TextNoteEvent.build("scale-$i"))
|
||||
arrivalNs.set(target, 0)
|
||||
val pubStart = System.nanoTime()
|
||||
pubClient.publishAndConfirm(event, setOf(relayUrl))
|
||||
|
||||
Reference in New Issue
Block a user