use anyhow::{Context, Result};
use chrono::Utc;
use rusqlite::Connection;
use std::path::{Path, PathBuf};
use super::connection::{drop_cached_connection, get_or_init_connection};
use super::{db_path_for, SQLITE_BUSY_TIMEOUT};
use crate::memory::config::MemoryConfig;
const SQLITE_CANTOPEN: i32 = 14;
const SQLITE_IOERR_TRUNCATE: i32 = 1546;
const SQLITE_IOERR_SHMOPEN: i32 = 4618;
const SQLITE_IOERR_SHMSIZE: i32 = 4874;
const SQLITE_IOERR_SHMMAP: i32 = 5386;
const SQLITE_IOERR_IN_PAGE: i32 = 8714;
#[allow(dead_code)]
pub(crate) fn is_transient_cold_start(err: &anyhow::Error) -> bool {
fn is_transient_sqlite(e: &(dyn std::error::Error + 'static)) -> bool {
if let Some(rusqlite::Error::SqliteFailure(ffi, _)) = e.downcast_ref::<rusqlite::Error>() {
return matches!(
ffi.extended_code,
SQLITE_CANTOPEN
| SQLITE_IOERR_TRUNCATE
| SQLITE_IOERR_SHMOPEN
| SQLITE_IOERR_SHMSIZE
| SQLITE_IOERR_SHMMAP
| SQLITE_IOERR_IN_PAGE
);
}
false
}
if is_transient_sqlite(err.root_cause()) {
return true;
}
let mut src: Option<&(dyn std::error::Error + 'static)> = Some(err.as_ref());
while let Some(cur) = src {
if is_transient_sqlite(cur) {
return true;
}
src = cur.source();
}
false
}
pub(crate) fn is_io_open_error(err: &anyhow::Error) -> bool {
if let Some(rusqlite::Error::SqliteFailure(f, _)) = err.downcast_ref::<rusqlite::Error>() {
return matches!(
f.extended_code,
SQLITE_CANTOPEN
| SQLITE_IOERR_TRUNCATE
| SQLITE_IOERR_SHMOPEN
| SQLITE_IOERR_SHMSIZE
| SQLITE_IOERR_SHMMAP
| SQLITE_IOERR_IN_PAGE
) || f.code == rusqlite::ErrorCode::CannotOpen;
}
let msg = format!("{err:#}").to_ascii_lowercase();
msg.contains("disk i/o error")
|| msg.contains("unable to open database file")
|| msg.contains("xshmmap")
|| msg.contains("truncate file")
}
pub(crate) fn try_cleanup_stale_files(db_path: &Path) -> bool {
let mut cleaned = false;
for suffix in &["-shm", "-wal"] {
let side = with_name_suffix(db_path, suffix);
if side.exists() && std::fs::remove_file(&side).is_ok() {
cleaned = true;
}
}
cleaned
}
fn with_name_suffix(path: &Path, suffix: &str) -> PathBuf {
let mut p = path.to_path_buf();
let name = p
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
p.set_file_name(format!("{name}{suffix}"));
p
}
fn quick_check_ok(db_path: &Path) -> Result<bool> {
let conn = Connection::open(db_path)
.with_context(|| format!("open for quick_check: {}", db_path.display()))?;
let _ = conn.busy_timeout(SQLITE_BUSY_TIMEOUT);
let result: String = conn
.query_row("PRAGMA quick_check(1)", [], |row| row.get(0))
.context("running PRAGMA quick_check")?;
Ok(result.eq_ignore_ascii_case("ok"))
}
#[allow(dead_code)]
pub(crate) fn recover_corrupt_db(config: &MemoryConfig) -> Result<bool> {
let db_path = db_path_for(config);
drop_cached_connection(config);
if db_path.exists() && matches!(quick_check_ok(&db_path), Ok(true)) {
return Ok(false);
}
let ts = Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
for suffix in &["", "-wal", "-shm"] {
let src = with_name_suffix(&db_path, suffix);
if !src.exists() {
continue;
}
let dst = with_name_suffix(&src, &format!(".corrupt-{ts}"));
std::fs::rename(&src, &dst).with_context(|| {
format!(
"failed to quarantine corrupt chunk DB file {} -> {}",
src.display(),
dst.display()
)
})?;
}
get_or_init_connection(config)
.context("failed to rebuild chunk DB schema after quarantining corrupt DB")?;
Ok(true)
}
#[cfg(test)]
#[path = "recovery_tests.rs"]
mod tests;