1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//! WAL checkpoint utilities for SQLite databases.
//!
//! Provides helpers to force WAL checkpoints after large write transactions,
//! preventing unbounded WAL growth that can lead to "database disk image is malformed".
use rusqlite::Connection;
use std::path::Path;
/// Force a WAL checkpoint on the given database file.
///
/// This should be called after large write transactions (bulk inserts, coverage ingestion,
/// watcher batch processing) to prevent unbounded WAL growth and reduce corruption risk.
///
/// # Errors
/// Returns `DatabaseBusy` if the checkpoint is blocked by active readers.
pub fn checkpoint_wal(db_path: &Path) -> Result<(), rusqlite::Error> {
let conn = Connection::open(db_path)?;
checkpoint_conn(&conn)
}
/// Checkpoint using an existing connection (avoids opening another connection).
///
/// # Errors
/// Returns `DatabaseBusy` if the checkpoint is blocked by active readers.
pub fn checkpoint_conn(conn: &Connection) -> Result<(), rusqlite::Error> {
let busy: i32 = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| row.get(0))?;
if busy != 0 {
return Err(rusqlite::Error::SqliteFailure(
rusqlite::ffi::Error {
code: rusqlite::ErrorCode::DatabaseBusy,
extended_code: 5,
},
Some("WAL checkpoint blocked by active readers".to_string()),
));
}
Ok(())
}
/// Checkpoint with retry logic for `DatabaseBusy` scenarios.
///
/// Retries up to `max_retries` times with exponential backoff.
pub fn checkpoint_conn_with_retry(
conn: &Connection,
max_retries: u32,
) -> Result<(), rusqlite::Error> {
let mut last_err = None;
for attempt in 0..max_retries {
match checkpoint_conn(conn) {
Ok(()) => return Ok(()),
Err(rusqlite::Error::SqliteFailure(err, _))
if err.code == rusqlite::ErrorCode::DatabaseBusy =>
{
let delay_ms = 2_u64.pow(attempt) * 10;
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
last_err = Some(err);
}
Err(e) => return Err(e),
}
}
Err(rusqlite::Error::SqliteFailure(
last_err.unwrap_or(rusqlite::ffi::Error {
code: rusqlite::ErrorCode::DatabaseBusy,
extended_code: 5,
}),
Some("WAL checkpoint failed after max retries".to_string()),
))
}