mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
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:
+56
@@ -233,6 +233,62 @@ class FilterIndexTest {
|
||||
assertEquals(setOf(s1, s2, s3), visited.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tagsAllFilterMatchesEventsCarryingAllTagValues() {
|
||||
// tagsAll keys the index on the first single-letter entry
|
||||
// exactly like `tags`. Events that match the index lookup
|
||||
// still have to pass `Filter.match` for the AND-of-values
|
||||
// semantics — the index is a super-set.
|
||||
val index = FilterIndex<Sub>()
|
||||
val s = Sub("s")
|
||||
index.register(Filter(tagsAll = mapOf("p" to listOf(pTag1))), s)
|
||||
|
||||
assertTrue(s in index.candidatesFor(event(tags = arrayOf(arrayOf("p", pTag1)))))
|
||||
assertFalse(s in index.candidatesFor(event(tags = arrayOf(arrayOf("p", pTag2)))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multiCharTagKeyFallsThroughToKindThenUnindexed() {
|
||||
// The index only buckets single-letter tag keys (NIP-12).
|
||||
// A filter with only multi-char tag keys should fall through
|
||||
// to the next dimension.
|
||||
val index = FilterIndex<Sub>()
|
||||
val withKind = Sub("withKind")
|
||||
val withoutKind = Sub("withoutKind")
|
||||
|
||||
index.register(Filter(tags = mapOf("alt" to listOf("foo")), kinds = listOf(7)), withKind)
|
||||
index.register(Filter(tags = mapOf("alt" to listOf("foo"))), withoutKind)
|
||||
|
||||
// withKind goes to KindKey(7); withoutKind to Unindexed.
|
||||
assertTrue(withKind in index.candidatesFor(event(kind = 7)))
|
||||
assertFalse(withKind in index.candidatesFor(event(kind = 1)))
|
||||
assertTrue(withoutKind in index.candidatesFor(event(kind = 1)))
|
||||
assertTrue(withoutKind in index.candidatesFor(event(kind = 7)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reRegisterAddsAdditionalKeysWithoutDuplicating() {
|
||||
// Calling register twice on the same subscriber unions the
|
||||
// bucket assignments — useful when an observer wants to
|
||||
// listen on multiple narrowing dimensions accumulated over
|
||||
// time. The bucket sets must dedupe so the subscriber
|
||||
// appears once in `candidatesFor`.
|
||||
val index = FilterIndex<Sub>()
|
||||
val s = Sub("s")
|
||||
index.register(Filter(authors = listOf(authorA)), s)
|
||||
index.register(Filter(kinds = listOf(7)), s)
|
||||
|
||||
// Author hit + kind hit = same subscriber, returned once.
|
||||
val cands = index.candidatesFor(event(pubkey = authorA, kind = 7))
|
||||
assertEquals(1, cands.size)
|
||||
assertTrue(s in cands)
|
||||
|
||||
// Unregister cleans both dimensions.
|
||||
index.unregister(s)
|
||||
assertTrue(index.candidatesFor(event(pubkey = authorA)).isEmpty())
|
||||
assertTrue(index.candidatesFor(event(kind = 7)).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun registerThenUnregisterLeavesNoStaleBuckets() {
|
||||
// Churn test — repeatedly add and remove a subscriber and
|
||||
|
||||
Reference in New Issue
Block a user