fix(test): close the store pool before salvaging its database #423

Merged
KofTwentyTwo merged 1 commits from fix/recovery-salvage-flake into dev 2026-08-15 01:13:29 +00:00
+23
View File
@@ -179,6 +179,18 @@ impl SqliteRecoveryEngine {
/// Salvage readable accounts and filter rules from damaged database or backup file,
/// re-creating a fresh SQLite file at `target_db_path`. Cryptographic key material for
/// the freshly re-created engine is provisioned from `secrets`.
///
/// # Precondition
///
/// The caller MUST have fully closed (via [`DatabaseEngine::close`], which awaits every
/// pooled connection's teardown) any engine still holding `target_db_path` before calling
/// this. Salvage unlinks `target_db_path` and its `-wal`/`-shm` companions and then creates
/// a brand-new database at that same path; a connection left open over the old file is torn
/// down asynchronously, and SQLite's close-time WAL handling operates on `-wal`/`-shm` *by
/// path*. A teardown landing after the unlink therefore collides with the fresh database's
/// own companions -- leaving the new, empty main file paired with a `-wal` describing the
/// old one, whose replay reads past end-of-file and fails with `SQLITE_IOERR_SHORT_READ`.
/// `DatabaseEngine::close_and_salvage` is the in-tree caller that honours this.
pub async fn salvage(
corrupted_db_path: &Path,
target_db_path: &Path,
@@ -530,6 +542,17 @@ mod tests {
let nsql = "SELECT * FROM emails WHERE subject CONTAINS 'Spam' ACTION DELETE";
let rule = nuncio_filter::NsqlParser::parse_rule("Spam Filter", 1, nsql).unwrap();
engine.save_filter_rule(&rule).await.unwrap();
// Closing before salvage is the documented precondition of
// `SqliteRecoveryEngine::salvage`, and it is what the production caller
// (`DatabaseEngine::close_and_salvage`) does. Merely dropping the engine is not
// equivalent: the pool's teardown is deferred onto the runtime, so the old
// connection's close-time WAL handling can land *after* salvage has unlinked
// `-wal`/`-shm` and created a fresh database at this same path -- pairing the new,
// empty main file with a `-wal` describing the old one and failing the subsequent
// page read with `SQLITE_IOERR_SHORT_READ`. `close()` awaits that teardown, so the
// window does not exist at all rather than merely being narrow.
engine.close().await;
}
// Step 2: Perform salvage recovery