feat(geode/quartz): [database] tuning knobs + periodic PRAGMA optimize

Backlog items 4-5 plumbing, config-gated and off by default so quartz
library defaults stay untouched for the app-side stores:

 - quartz: SQLiteEventStore/EventStore accept extraPragmas (applied on
   every pooled connection AFTER the built-in configuration, so they
   can override it) and expose optimize() — an analysis_limit-bounded
   PRAGMA optimize for incremental planner-stats refresh.
 - geode: [database] readers / mmap_size / temp_store_memory map onto
   the store; optimize_interval_seconds drives a maintenance coroutine
   (cancelled first in the shutdown hook, before the store closes).

The A/B verdict on whether the example config should RECOMMEND any of
these on container-class hardware follows in the next commit — the
knobs themselves are operator tools worth having either way, since
mmap/temp-store value is hardware-dependent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtDNpayEYvJH7QuPswND3A
This commit is contained in:
Claude
2026-07-04 00:48:22 +00:00
parent fd6662ca30
commit 79373672df
7 changed files with 202 additions and 5 deletions
+16
View File
@@ -46,6 +46,22 @@ path = "/"
in_memory = false
file = "/var/lib/geode/events.db"
# Reader-connection pool size (file-backed stores only). Default 4.
# readers = 4
# PRAGMA mmap_size in bytes — maps the db file into memory so reads
# skip the pread syscall. Off by default (SQLite default).
# mmap_size = 268435456
# PRAGMA temp_store = MEMORY — RAM instead of temp files for large
# sorts. Off by default.
# temp_store_memory = true
# Refresh query-planner statistics (PRAGMA optimize) every N seconds.
# Incremental and usually a no-op; keeps the planner from drifting
# onto the wrong index as the corpus grows. Off by default.
# optimize_interval_seconds = 3600
[options]
# Drop events whose Schnorr signature does not verify. Strongly
# recommended for any relay accepting traffic from real clients.
@@ -37,9 +37,14 @@ import com.vitorpamplona.quartz.nip01Core.relay.server.policies.OptionalAuthPoli
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.RejectFutureEventsPolicy
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.VerifyAuthOnlyPolicy
import com.vitorpamplona.quartz.nip01Core.relay.server.policies.VerifyPolicy
import com.vitorpamplona.quartz.nip01Core.store.IEventStore
import com.vitorpamplona.quartz.nip01Core.store.sqlite.EventStore
import com.vitorpamplona.quartz.nip77Negentropy.NegentropySettings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
/**
@@ -126,11 +131,20 @@ fun main(args: Array<String>) {
cliInfoFile?.let { RelayInfo.fromFile(it) }
?: config.resolveInfo(fullTextSearch)
val store: IEventStore =
// Deployment tuning from `[database]` — off by default; the quartz
// library defaults stay tuned for the app-side stores.
val extraPragmas =
buildList {
config.database.mmap_size?.let { add("PRAGMA mmap_size = $it;") }
if (config.database.temp_store_memory) add("PRAGMA temp_store = MEMORY;")
}
val store =
EventStore(
dbName = dbFile,
relay = advertisedUrl,
indexStrategy = relayIndexingStrategy(fullTextSearch, config.negentropy.live_index),
numReaders = config.database.readers ?: 4,
extraPragmas = extraPragmas,
)
val policyBuilder: () -> IRelayPolicy = {
@@ -223,12 +237,26 @@ fun main(args: Array<String>) {
MirrorWorker(upstreams, relay.server).also { it.start() }
}
// Periodic query-planner statistics refresh (`PRAGMA optimize`).
// Incremental and usually a no-op; failures are swallowed — a missed
// refresh only means slightly staler planner stats until the next tick.
val maintenanceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
config.database.optimize_interval_seconds?.let { secs ->
maintenanceScope.launch {
while (true) {
delay(secs * 1000)
runCatching { store.optimize() }
}
}
}
Runtime.getRuntime().addShutdownHook(
Thread {
// Each step wrapped so a throw in any stage doesn't skip
// `relay.close()` (which closes the SQLite store). The mirror
// goes first: stop pulling new events before the queue and
// store beneath it shut down.
// and maintenance go first: stop touching the store before the
// queue and store beneath them shut down.
runCatching { maintenanceScope.cancel() }
runCatching { mirror?.close() }
runCatching { server.stop() }
runCatching { relay.close() }
@@ -106,6 +106,30 @@ data class StaticConfig(
/** True keeps an in-memory SQLite db (default — events vanish on restart). */
val in_memory: Boolean = true,
val file: String? = null,
/**
* Reader-connection pool size. `null` keeps quartz's default (4).
* Only meaningful for file-backed stores; in-memory databases
* share the single writer connection regardless.
*/
val readers: Int? = null,
/**
* `PRAGMA mmap_size` in bytes, e.g. `268435456` for 256 MiB.
* Maps the database file into memory so reads skip the pread
* syscall + page-cache copy. `null` keeps SQLite's default (off).
*/
val mmap_size: Long? = null,
/**
* `PRAGMA temp_store = MEMORY` — keeps sort/temp b-trees for
* large queries in RAM instead of temp files.
*/
val temp_store_memory: Boolean = false,
/**
* Refresh query-planner statistics (`PRAGMA analysis_limit;
* PRAGMA optimize`) every this many seconds. Incremental and
* usually a no-op, but keeps the planner from drifting onto the
* wrong index as the corpus grows/changes shape. `null` = never.
*/
val optimize_interval_seconds: Long? = null,
)
data class OptionsSection(
@@ -48,6 +48,34 @@ class StaticConfigTest {
assertEquals(false, c.options.verify_signatures)
}
@Test
fun parsesDatabaseTuningKnobs() {
val toml =
"""
[database]
in_memory = false
file = "/tmp/x.db"
readers = 8
mmap_size = 268435456
temp_store_memory = true
optimize_interval_seconds = 3600
""".trimIndent()
val c = StaticConfig.fromToml(toml)
assertEquals(8, c.database.readers)
assertEquals(268435456L, c.database.mmap_size)
assertEquals(true, c.database.temp_store_memory)
assertEquals(3600L, c.database.optimize_interval_seconds)
// And all knobs default to off/null so plain configs are untouched.
val d = StaticConfig.fromToml("")
assertEquals(null, d.database.readers)
assertEquals(null, d.database.mmap_size)
assertEquals(false, d.database.temp_store_memory)
assertEquals(null, d.database.optimize_interval_seconds)
}
@Test
fun mirrorSectionDefaultsToEmpty() {
assertTrue(StaticConfig.fromToml("").mirror.isEmpty())
@@ -39,8 +39,14 @@ class EventStore(
dbName: String? = "events.db",
override val relay: NormalizedRelayUrl? = "wss://quartz.local/".normalizeRelayUrl(),
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
numReaders: Int = 4,
/** See [SQLiteEventStore.extraPragmas] — deployment-specific tuning. */
extraPragmas: List<String> = emptyList(),
) : IEventStore {
val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relay, indexStrategy)
val store = SQLiteEventStore(BundledSQLiteDriver(), dbName, relay, indexStrategy, numReaders, extraPragmas)
/** Incremental planner-stats refresh — see [SQLiteEventStore.optimize]. */
suspend fun optimize() = store.optimize()
override suspend fun insert(event: Event) = store.insertEvent(event)
@@ -48,6 +48,15 @@ class SQLiteEventStore(
val relay: NormalizedRelayUrl? = null,
val indexStrategy: IndexingStrategy = DefaultIndexingStrategy(),
val numReaders: Int = 4,
/**
* Extra `PRAGMA` statements run on every pooled connection after the
* built-in configuration (cache size, WAL, busy timeout, …), so they
* can override it. Deployment-specific tuning goes here — e.g.
* `PRAGMA mmap_size = 268435456;` or `PRAGMA temp_store = MEMORY;`
* on server hardware — while the library defaults stay tuned for the
* app-side stores. Statements must be self-contained SQL.
*/
val extraPragmas: List<String> = emptyList(),
) {
companion object {
const val DATABASE_VERSION = 4
@@ -133,6 +142,9 @@ class SQLiteEventStore(
// upgrading its snapshot. With it, SQLite retries internally
// for up to N ms before giving up. Matches Room's default.
db.execSQL("PRAGMA busy_timeout = 5000;")
// Deployment overrides last, so they win over the defaults.
extraPragmas.forEach { db.execSQL(it) }
},
onMigrate = { db ->
val currentVersion = getUserVersion(db)
@@ -244,6 +256,20 @@ class SQLiteEventStore(
db.execSQL("ANALYZE")
}
/**
* Incremental planner-statistics refresh for long-running relays.
* `PRAGMA optimize` re-analyzes only tables whose content changed
* enough to matter, and `analysis_limit` bounds each table's scan —
* so a periodic call stays cheap (typically no-op) while keeping the
* planner from drifting onto the wrong index as the corpus grows.
* Runs on the writer connection; call it off the hot path.
*/
suspend fun optimize() =
pool.useWriter { db ->
db.execSQL("PRAGMA analysis_limit = 400;")
db.execSQL("PRAGMA optimize;")
}
/**
* Live-index mutations gathered during one write transaction and
* applied only after its COMMIT — a rolled-back row (savepoint or
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip01Core.store.sqlite
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* [SQLiteEventStore.extraPragmas] must actually reach every pooled
* connection (after the built-in defaults, so they can override), and
* [SQLiteEventStore.optimize] must run cleanly on a live store.
*/
class ExtraPragmasTest {
private suspend fun SQLiteEventStore.pragmaValue(name: String): Long =
pool.useReader { db ->
db.prepare("PRAGMA $name").use { stmt ->
stmt.step()
stmt.getLong(0)
}
}
@Test
fun extraPragmasApplyToConnections() =
runTest {
// temp_store: 0=default, 2=MEMORY.
val store =
SQLiteEventStore(
dbName = null,
extraPragmas = listOf("PRAGMA temp_store = MEMORY;"),
)
assertEquals(2L, store.pragmaValue("temp_store"))
store.close()
}
@Test
fun defaultsHaveNoExtraPragmas() =
runTest {
val store = SQLiteEventStore(dbName = null)
assertEquals(0L, store.pragmaValue("temp_store"))
store.close()
}
@Test
fun optimizeRunsCleanly() =
runTest {
val store = SQLiteEventStore(dbName = null)
store.optimize()
store.close()
}
}