fix(database): route all one-shot DB writes through the merge drain barrier (#6233)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
James Rich
2026-07-11 19:12:57 -05:00
committed by GitHub
parent 4e48e64e78
commit e48640e473
7 changed files with 233 additions and 143 deletions

View File

@@ -383,9 +383,15 @@ open class DatabaseManager(
}
}
/** Atomically snapshots the active DB and registers a writer against it. Returns null if none is open. */
private suspend fun beginWrite(): MeshtasticDatabase? = writerTrackerMutex.withLock {
val db = _currentDb.value ?: return@withLock null
/**
* Atomically snapshots the active DB and registers a writer against it. Before the first [switchActiveDatabase]
* `_currentDb` is still null, so fall back to the public [currentDb] view — the eagerly-opened default DB — giving
* withDb the exact DB-resolution semantics of a direct `currentDb.value` caller. Desktop never calls [init] until a
* device is selected, so without the fallback pre-connection writes (quick-chat actions, firmware/hardware cache
* refreshes) would silently no-op there instead of landing in the default DB the pre-connection flows read from.
*/
private suspend fun beginWrite(): MeshtasticDatabase = writerTrackerMutex.withLock {
val db = _currentDb.value ?: currentDb.value
activeWriters[db] = (activeWriters[db] ?: 0) + 1
db
}
@@ -433,7 +439,7 @@ open class DatabaseManager(
@Suppress("ReturnCount", "ThrowsCount", "TooGenericExceptionCaught", "CyclomaticComplexMethod")
private suspend fun <T> withCurrentDb(block: suspend (MeshtasticDatabase) -> T): T? {
val db = beginWrite() ?: return null
val db = beginWrite()
val active = currentDbName
markLastUsed(active)
try {

View File

@@ -21,6 +21,14 @@ import kotlinx.coroutines.flow.StateFlow
/**
* Provides multiplatform access to the current [MeshtasticDatabase] and a safe transactional helper. Platform
* implementations manage the concrete lifecycle (Room on Android, etc.).
*
* **Write policy:** every one-shot DAO *write* (insert/upsert/update/delete/clear) must go through [withDb], never
* `currentDb.value` directly. [withDb] registers the write with the cross-transport merge drain barrier (see
* [DatabaseManager.associateDevice]) so a merge can't snapshot a database while the write is still in flight and lose
* it when that database is retired — and it retries once if the pool closes under a concurrent DB switch. Direct
* `currentDb.value` is fine for one-shot *reads* (a torn read against a just-retired DB is recoverable and reads don't
* need drain visibility), and `currentDb` itself is the right latch for Flow/Paging factories, which must re-latch on
* DB switch and must stay out of [withDb]'s containment lane.
*/
interface DatabaseProvider {
/** Reactive stream of the currently active database instance. */