mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
fix(quartz/sqlite): serialise writes via a Room-style connection pool
androidx.sqlite SQLiteConnection is not thread-safe; SQLiteEventStore shared a single lazy connection across all callers, so two coroutines calling insertEvent() at the same time would race on BEGIN IMMEDIATE and the modules' prepared statements, surfacing as "cannot start a transaction within a transaction" or SQLITE_MISUSE. Mirror Room's design: introduce SQLiteConnectionPool with one writer connection guarded by a coroutine Mutex and N reader connections handed out via a Channel-as-semaphore (file-backed DBs only; in-memory DBs share the writer because each ":memory:" connection is a separate DB). Convert IEventStore + SQLiteEventStore + EventStore + FsEventStore + LiveEventStore to suspend, route writes through useWriter and reads through useReader. RelaySession now launches handleEvent / handleCount on its scope. CLI Context helpers and StoreCommands.sweepExpired pick up suspend. Add ParallelInsertTest to lock the behaviour in: 8 coroutines × 200 inserts, parallel reads alongside writes, transaction batches across coroutines, and a reopen smoke test all pass against a file-backed DB. https://claude.ai/code/session_016b5kSSbtDS3Ead6pN3Xqt5
This commit is contained in:
@@ -173,7 +173,7 @@ class Context(
|
||||
* publish from", which mirrors `User.outboxRelays()` in the
|
||||
* Android app.
|
||||
*/
|
||||
fun outboxRelays(): Set<NormalizedRelayUrl> =
|
||||
suspend fun outboxRelays(): Set<NormalizedRelayUrl> =
|
||||
relaysOf(identity.pubKeyHex)?.writeRelaysNorm()?.takeIf { it.isNotEmpty() }?.toSet()
|
||||
?: DefaultNIP65RelaySet
|
||||
|
||||
@@ -181,7 +181,7 @@ class Context(
|
||||
* DM inbox relays (NIP-17 kind:10050) for this account. Falls back
|
||||
* to [DefaultDMRelayList] when no kind:10050 has been seen.
|
||||
*/
|
||||
fun inboxRelays(): Set<NormalizedRelayUrl> =
|
||||
suspend fun inboxRelays(): Set<NormalizedRelayUrl> =
|
||||
dmInboxOf(identity.pubKeyHex)?.relays()?.takeIf { it.isNotEmpty() }?.toSet()
|
||||
?: DefaultDMRelayList.toSet()
|
||||
|
||||
@@ -190,12 +190,12 @@ class Context(
|
||||
* back to [outboxRelays] when no kind:10051 has been seen — same
|
||||
* fallback the Android app uses for KeyPackage discovery.
|
||||
*/
|
||||
fun keyPackageRelays(): Set<NormalizedRelayUrl> =
|
||||
suspend fun keyPackageRelays(): Set<NormalizedRelayUrl> =
|
||||
keyPackageRelaysOf(identity.pubKeyHex)?.relays()?.takeIf { it.isNotEmpty() }?.toSet()
|
||||
?: outboxRelays()
|
||||
|
||||
/** Union of all three buckets. */
|
||||
fun anyRelays(): Set<NormalizedRelayUrl> = outboxRelays() + inboxRelays() + keyPackageRelays()
|
||||
suspend fun anyRelays(): Set<NormalizedRelayUrl> = outboxRelays() + inboxRelays() + keyPackageRelays()
|
||||
|
||||
/**
|
||||
* Seed relays for "look up someone we know nothing about" queries —
|
||||
@@ -208,7 +208,7 @@ class Context(
|
||||
* most reliable place to find a stranger's replaceable events even when
|
||||
* we and they have completely disjoint relay configurations.
|
||||
*/
|
||||
fun bootstrapRelays(): Set<NormalizedRelayUrl> =
|
||||
suspend fun bootstrapRelays(): Set<NormalizedRelayUrl> =
|
||||
buildSet {
|
||||
addAll(anyRelays())
|
||||
addAll(DefaultNIP65RelaySet)
|
||||
@@ -319,7 +319,7 @@ class Context(
|
||||
* Every event-arrival path in the CLI funnels through this method
|
||||
* so that [store] is the authoritative cache of what Amy has seen.
|
||||
*/
|
||||
fun verifyAndStore(event: Event): Boolean {
|
||||
suspend fun verifyAndStore(event: Event): Boolean {
|
||||
if (!event.verify()) {
|
||||
System.err.println("[cli] dropped event ${event.id.take(8)} kind=${event.kind} — bad signature")
|
||||
return false
|
||||
@@ -342,7 +342,7 @@ class Context(
|
||||
* this user. Callers that need a network fetch on miss should fall
|
||||
* back to [drain] explicitly — this helper never hits the network.
|
||||
*/
|
||||
fun profileOf(pubKey: HexKey): MetadataEvent? =
|
||||
suspend fun profileOf(pubKey: HexKey): MetadataEvent? =
|
||||
store
|
||||
.query<Event>(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(MetadataEvent.KIND), limit = 1),
|
||||
@@ -352,7 +352,7 @@ class Context(
|
||||
* Latest known kind:10002 advertised relay list (NIP-65) for
|
||||
* [pubKey]. `null` when Amy has never seen one.
|
||||
*/
|
||||
fun relaysOf(pubKey: HexKey): AdvertisedRelayListEvent? =
|
||||
suspend fun relaysOf(pubKey: HexKey): AdvertisedRelayListEvent? =
|
||||
store
|
||||
.query<Event>(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(AdvertisedRelayListEvent.KIND), limit = 1),
|
||||
@@ -363,7 +363,7 @@ class Context(
|
||||
* `null` if Amy has never observed one. Useful for follow-graph
|
||||
* lookups without re-hitting relays.
|
||||
*/
|
||||
fun contactsOf(pubKey: HexKey): ContactListEvent? =
|
||||
suspend fun contactsOf(pubKey: HexKey): ContactListEvent? =
|
||||
store
|
||||
.query<Event>(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(ContactListEvent.KIND), limit = 1),
|
||||
@@ -374,7 +374,7 @@ class Context(
|
||||
* for [pubKey], or `null` if Amy has never observed one. Used by
|
||||
* `dm send` to resolve where to deliver a wrap.
|
||||
*/
|
||||
fun dmInboxOf(pubKey: HexKey): ChatMessageRelayListEvent? =
|
||||
suspend fun dmInboxOf(pubKey: HexKey): ChatMessageRelayListEvent? =
|
||||
store
|
||||
.query<Event>(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(ChatMessageRelayListEvent.KIND), limit = 1),
|
||||
@@ -386,7 +386,7 @@ class Context(
|
||||
* `marmot key-package check` and `marmot await key-package` to
|
||||
* locate where the recipient publishes their KeyPackages.
|
||||
*/
|
||||
fun keyPackageRelaysOf(pubKey: HexKey): KeyPackageRelayListEvent? =
|
||||
suspend fun keyPackageRelaysOf(pubKey: HexKey): KeyPackageRelayListEvent? =
|
||||
store
|
||||
.query<Event>(
|
||||
Filter(authors = listOf(pubKey), kinds = listOf(KeyPackageRelayListEvent.KIND), limit = 1),
|
||||
@@ -405,7 +405,7 @@ class Context(
|
||||
* we'll still hand back the old list. Commands that care can drain
|
||||
* (which re-populates the cache) or expose a `--refresh` flag.
|
||||
*/
|
||||
fun cachedRelayListsOf(pubKey: HexKey): RecipientRelayFetcher.Lists? {
|
||||
suspend fun cachedRelayListsOf(pubKey: HexKey): RecipientRelayFetcher.Lists? {
|
||||
val dm = dmInboxOf(pubKey)
|
||||
val kp = keyPackageRelaysOf(pubKey)
|
||||
val nip65 = relaysOf(pubKey)
|
||||
|
||||
@@ -142,7 +142,7 @@ object FeedCommand {
|
||||
* an arbitrary `--author` we have no idea where they publish, so we
|
||||
* widen to the bootstrap union.
|
||||
*/
|
||||
private fun relaysForReadingFeed(
|
||||
private suspend fun relaysForReadingFeed(
|
||||
ctx: Context,
|
||||
mode: String,
|
||||
): Set<NormalizedRelayUrl> =
|
||||
|
||||
@@ -226,7 +226,7 @@ object ProfileCommands {
|
||||
* else's profile, fall back to the bootstrap union so we still find a
|
||||
* kind:0 even when our relay set and theirs are disjoint.
|
||||
*/
|
||||
private fun relaysForReadingProfile(
|
||||
private suspend fun relaysForReadingProfile(
|
||||
ctx: Context,
|
||||
isSelf: Boolean,
|
||||
): Set<NormalizedRelayUrl> =
|
||||
|
||||
@@ -137,7 +137,7 @@ object RelayCommands {
|
||||
}
|
||||
}
|
||||
|
||||
private fun list(dataDir: DataDir): Int {
|
||||
private suspend fun list(dataDir: DataDir): Int {
|
||||
val ctx = Context.open(dataDir)
|
||||
try {
|
||||
val self = ctx.identity.pubKeyHex
|
||||
|
||||
@@ -132,7 +132,7 @@ object StoreCommands {
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun sweepExpired(dataDir: DataDir): Int =
|
||||
private suspend fun sweepExpired(dataDir: DataDir): Int =
|
||||
withStore(dataDir) { store ->
|
||||
val expiresAtDir = dataDir.eventsDir.toPath().resolve("idx/expires_at")
|
||||
val before = countEntries(expiresAtDir)
|
||||
|
||||
Reference in New Issue
Block a user