mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
docs(quartz/store): explain the connection pool and the suspend API
- Add a Concurrency section to the SQLite store README covering the Room-style 1-writer + N-reader pool, the in-memory degradation, and the non-reentrant Mutex contract. - Refresh the SQLite "How to Use" examples to call out the suspend context and recommend transaction-batching for hot inserts. - Switch the ExpirationWorker example from Worker to CoroutineWorker now that deleteExpiredEvents is suspend. - Note in the FS README that the IEventStore API is suspend even though the FS layer keeps a synchronous flock manager (the withWriteLock helper is inline so suspend bodies pass through). - Update the FsMaintenanceTest description to match the coroutine-based concurrency test. - Document the Mutex non-reentrancy footgun in SQLiteConnectionPool's KDoc so module logic doesn't try to re-enter the pool from inside useWriter. https://claude.ai/code/session_016b5kSSbtDS3Ead6pN3Xqt5
This commit is contained in:
+48
-5
@@ -80,10 +80,39 @@ store.query(
|
||||
)
|
||||
```
|
||||
|
||||
## Concurrency
|
||||
|
||||
`androidx.sqlite.SQLiteConnection` is not thread-safe — same contract as
|
||||
`sqlite3*` in the C API. To support concurrent inserts and reads from
|
||||
multiple coroutines, `SQLiteEventStore` owns a Room-style
|
||||
[`SQLiteConnectionPool`](SQLiteConnectionPool.kt):
|
||||
|
||||
- **One writer connection**, guarded by a coroutine `Mutex`. SQLite only
|
||||
allows one writer at the file level anyway, so serialising here costs
|
||||
nothing — it just queues callers cooperatively instead of crashing
|
||||
them on `BEGIN IMMEDIATE`.
|
||||
- **N reader connections** (default 4), handed out from a `Channel` that
|
||||
doubles as a semaphore. Under WAL (`PRAGMA journal_mode = WAL`)
|
||||
readers run in parallel with the writer and with each other.
|
||||
|
||||
For in-memory databases (`dbName == null`) the pool degrades to a
|
||||
single shared connection — every fresh `:memory:` connection would
|
||||
otherwise be a *separate* DB. Writes still serialise correctly; reads
|
||||
just take the same writer mutex.
|
||||
|
||||
The whole public API on `EventStore` / `SQLiteEventStore` is therefore
|
||||
`suspend`. Callers must be in a coroutine; on Android, schedule
|
||||
maintenance work as a `CoroutineWorker`.
|
||||
|
||||
`Mutex` is non-reentrant: do not call `eventStore.query(...)` from
|
||||
inside a `transaction { ... }` body. The transaction body itself
|
||||
already holds the writer connection — query against the
|
||||
`SQLiteConnection` handed to your block instead.
|
||||
|
||||
## How to Use
|
||||
|
||||
The `EventStore` class provides a high-level interface for interacting with the event database.
|
||||
It is initialized with a `SQLiteDatabase` instance, and it manages the underlying tables and query planning.
|
||||
It owns the underlying [`SQLiteConnectionPool`](SQLiteConnectionPool.kt) and the query planner.
|
||||
|
||||
### Initialization
|
||||
|
||||
@@ -95,7 +124,7 @@ val eventStore = EventStore("dbname.db", relayUrlIdentifier)
|
||||
|
||||
### Querying Events
|
||||
|
||||
To query events, use the `query` method with one or more `Filter` objects:
|
||||
To query events, use the `query` method with one or more `Filter` objects (in a coroutine):
|
||||
|
||||
```kotlin
|
||||
val filters = listOf(
|
||||
@@ -129,6 +158,18 @@ Insert a single event using the `insert` method:
|
||||
eventStore.insert(event)
|
||||
```
|
||||
|
||||
For batch inserts, prefer a single `transaction` — one `BEGIN`/`COMMIT`
|
||||
per batch is roughly an order of magnitude faster on WAL than one per
|
||||
event:
|
||||
|
||||
```kotlin
|
||||
eventStore.transaction {
|
||||
insert(event1)
|
||||
insert(event2)
|
||||
insert(event3)
|
||||
}
|
||||
```
|
||||
|
||||
### Deleting Events
|
||||
|
||||
Events should be deleted by adding a DeletionRequest or a VanishRequest to the db, but to manually
|
||||
@@ -154,11 +195,13 @@ The store exposes a `deleteExpiredEvents` to be used in a periodic clean up proc
|
||||
should use a WorkManager or a coroutine to periodically call `store.deleteExpiredEvents()`. We
|
||||
recommend a 15-minute window to remove recently expired events from the database.
|
||||
|
||||
Here's an example of a Worker that should be added to your application class.
|
||||
Here's an example of a Worker that should be added to your application class. Use
|
||||
`CoroutineWorker` (not `Worker`) — `deleteExpiredEvents()` is a `suspend` function.
|
||||
|
||||
```kotlin
|
||||
class ExpirationWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
|
||||
override fun doWork(): Result {
|
||||
class ExpirationWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||
CoroutineWorker(appContext, workerParams) {
|
||||
override suspend fun doWork(): Result {
|
||||
YourApplication.store.deleteExpiredEvents()
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
+6
@@ -59,6 +59,12 @@ import kotlinx.coroutines.sync.withLock
|
||||
* per-connection in SQLite — `journal_mode=WAL` is the only
|
||||
* database-wide one; subsequent connections inherit it).
|
||||
* 3. [close] drains the reader channel and closes every connection.
|
||||
*
|
||||
* Reentrancy: [Mutex] is **not** reentrant — calling [useWriter] (or, on
|
||||
* an in-memory DB, [useReader]) from inside an already-acquired
|
||||
* [useWriter] block deadlocks. Module logic that runs under [useWriter]
|
||||
* (e.g. `innerInsertEvent`) must operate on the `SQLiteConnection`
|
||||
* handed to its block; it must not re-enter the pool.
|
||||
*/
|
||||
class SQLiteConnectionPool(
|
||||
val driver: SQLiteDriver,
|
||||
|
||||
@@ -128,6 +128,11 @@ lock — atomic-rename writes mean readers see either the pre- or
|
||||
post-mutation state, and `NoSuchFileException` on a just-unlinked
|
||||
candidate is silently skipped.
|
||||
|
||||
The `IEventStore` API is `suspend`. The flock manager itself is
|
||||
synchronous (`ReentrantLock` + `FileChannel.lock`); each suspend
|
||||
public method just brackets its work in `lockManager.withWriteLock {
|
||||
... }`, which is `inline` so suspend bodies pass through.
|
||||
|
||||
## Usage
|
||||
|
||||
### Initialisation
|
||||
@@ -301,7 +306,7 @@ Tests live under
|
||||
| `FsExpirationTest` | NIP-40 future / past / equal-now / sweep / non-positive |
|
||||
| `FsVanishTest` | NIP-62 cascade / block / strongest-cutoff-wins / per-relay scoping |
|
||||
| `FsSearchTest` | tokenizer behaviour, single-token / AND-of-tokens, ordering, reopen |
|
||||
| `FsMaintenanceTest` | flock, transaction commit + propagated exceptions, re-entrant lock, scrub, compact, two-thread concurrency |
|
||||
| `FsMaintenanceTest` | flock, transaction commit + propagated exceptions, re-entrant lock, scrub, compact, concurrent inserts from multiple coroutines on `Dispatchers.IO` |
|
||||
| `FsParityTest` | drive both this store and SQLite with identical streams and assert results match |
|
||||
|
||||
```bash
|
||||
|
||||
Reference in New Issue
Block a user