mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Further investigation of the sqlite incidents showed that the problems are due to corruption from the older version of SQLite that we recently upgraded, and that the data is truly corrupted in the root database -- recovery of all data is not possible. Given that the data is reconstructable from the rollouts on disk, we should just auto-backup the database and let codex rebuild the rollout info from the disk rollouts. The new behavior is that appserver auto-backs-up and rebuilds (with logs reflecting that behavior). The CLI now pops a message letting you know this happened and the paths of the backed-up corrupt db and the new database. There is also context added so that the desktop app can read the rebuild info from it and inform the user with it.
244 lines
7.8 KiB
Rust
244 lines
7.8 KiB
Rust
//! Backup-and-rebuild support for Codex runtime SQLite databases.
|
|
//!
|
|
//! Codex keeps several independent runtime SQLite databases under one SQLite
|
|
//! home. When SQLite reports that one of them is corrupt, automatic recovery
|
|
//! moves only that database file and its sidecars into a backup folder so the
|
|
//! other databases keep their data.
|
|
|
|
use std::borrow::Cow;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
const BACKUP_DIR_NAME: &str = "db-backups";
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct RuntimeDbBackup {
|
|
/// Path where the runtime database or sidecar lived before it was moved.
|
|
pub original_path: PathBuf,
|
|
/// Path where the runtime database or sidecar was backed up.
|
|
pub backup_path: PathBuf,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct RuntimeDbInitError {
|
|
label: &'static str,
|
|
operation: &'static str,
|
|
path: PathBuf,
|
|
source: anyhow::Error,
|
|
}
|
|
|
|
impl RuntimeDbInitError {
|
|
pub(crate) fn new(
|
|
label: &'static str,
|
|
operation: &'static str,
|
|
path: &Path,
|
|
source: anyhow::Error,
|
|
) -> Self {
|
|
Self {
|
|
label,
|
|
operation,
|
|
path: path.to_path_buf(),
|
|
source,
|
|
}
|
|
}
|
|
|
|
fn path(&self) -> &Path {
|
|
self.path.as_path()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for RuntimeDbInitError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"failed to {} {} at {}: {}",
|
|
self.operation,
|
|
self.label,
|
|
self.path.display(),
|
|
self.source
|
|
)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for RuntimeDbInitError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
Some(self.source.as_ref())
|
|
}
|
|
}
|
|
|
|
/// Move one Codex runtime SQLite database out of the way so that database can
|
|
/// be recreated without discarding unrelated runtime databases.
|
|
pub async fn backup_runtime_db_for_fresh_start(
|
|
db_path: &Path,
|
|
) -> std::io::Result<Vec<RuntimeDbBackup>> {
|
|
let sqlite_home = db_path.parent().ok_or_else(|| {
|
|
std::io::Error::other(format!(
|
|
"database path does not have a parent directory: {}",
|
|
db_path.display()
|
|
))
|
|
})?;
|
|
match tokio::fs::metadata(sqlite_home).await {
|
|
Ok(metadata) if metadata.is_dir() => backup_runtime_db_files(db_path).await,
|
|
Ok(_) => backup_blocking_sqlite_home(sqlite_home).await,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
tokio::fs::create_dir_all(sqlite_home).await?;
|
|
Err(std::io::Error::other(format!(
|
|
"no Codex runtime database files were found to back up for {}",
|
|
db_path.display()
|
|
)))
|
|
}
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
pub fn runtime_db_path_for_corruption_error(err: &anyhow::Error) -> Option<PathBuf> {
|
|
if !is_sqlite_corruption_error(err) {
|
|
return None;
|
|
}
|
|
err.chain()
|
|
.find_map(|source| source.downcast_ref::<RuntimeDbInitError>())
|
|
.map(|err| err.path().to_path_buf())
|
|
}
|
|
|
|
pub fn is_sqlite_corruption_error(err: &anyhow::Error) -> bool {
|
|
err.chain().any(sqlite_error_source_is_corruption)
|
|
}
|
|
|
|
fn sqlite_error_source_is_corruption(source: &(dyn std::error::Error + 'static)) -> bool {
|
|
let Some(err) = source.downcast_ref::<sqlx::Error>() else {
|
|
return false;
|
|
};
|
|
let sqlx::Error::Database(database_error) = err else {
|
|
return false;
|
|
};
|
|
sqlite_error_detail_is_corruption(database_error.message())
|
|
|| database_error
|
|
.code()
|
|
.is_some_and(sqlite_database_code_is_corruption)
|
|
}
|
|
|
|
fn sqlite_database_code_is_corruption(code: Cow<'_, str>) -> bool {
|
|
matches!(
|
|
code.as_ref().to_ascii_lowercase().as_str(),
|
|
"11" | "26" | "sqlite_corrupt" | "sqlite_notadb"
|
|
)
|
|
}
|
|
|
|
pub fn sqlite_error_detail_is_corruption(detail: &str) -> bool {
|
|
let detail = detail.to_ascii_lowercase();
|
|
detail.contains("database disk image is malformed")
|
|
|| detail.contains("database schema is malformed")
|
|
|| detail.contains("database is corrupt")
|
|
|| detail.contains("file is not a database")
|
|
|| detail.contains("sqlite_corrupt")
|
|
|| detail.contains("sqlite_notadb")
|
|
|| detail.contains("(code: 11)")
|
|
|| detail.contains("(code: 26)")
|
|
}
|
|
|
|
pub fn sqlite_error_detail_is_lock(detail: &str) -> bool {
|
|
let detail = detail.to_ascii_lowercase();
|
|
detail.contains("database is locked") || detail.contains("database is busy")
|
|
}
|
|
|
|
async fn backup_runtime_db_files(db_path: &Path) -> std::io::Result<Vec<RuntimeDbBackup>> {
|
|
let sqlite_home = db_path.parent().ok_or_else(|| {
|
|
std::io::Error::other(format!(
|
|
"database path does not have a parent directory: {}",
|
|
db_path.display()
|
|
))
|
|
})?;
|
|
backup_sqlite_paths(sqlite_home, sqlite_paths(db_path)).await
|
|
}
|
|
|
|
async fn backup_sqlite_paths(
|
|
sqlite_home: &Path,
|
|
paths: impl IntoIterator<Item = PathBuf>,
|
|
) -> std::io::Result<Vec<RuntimeDbBackup>> {
|
|
let backup_dir = create_unique_backup_dir(sqlite_home.join(BACKUP_DIR_NAME).as_path()).await?;
|
|
let mut backups = Vec::new();
|
|
|
|
for path in paths {
|
|
if tokio::fs::try_exists(path.as_path()).await? {
|
|
let backup_path = backup_dir.join(file_name(path.as_path())?);
|
|
tokio::fs::rename(path.as_path(), backup_path.as_path()).await?;
|
|
backups.push(RuntimeDbBackup {
|
|
original_path: path,
|
|
backup_path,
|
|
});
|
|
}
|
|
}
|
|
|
|
if backups.is_empty() {
|
|
let _ = tokio::fs::remove_dir(backup_dir).await;
|
|
return Err(std::io::Error::other(
|
|
"no Codex runtime database files were found to back up",
|
|
));
|
|
}
|
|
|
|
Ok(backups)
|
|
}
|
|
|
|
async fn backup_blocking_sqlite_home(sqlite_home: &Path) -> std::io::Result<Vec<RuntimeDbBackup>> {
|
|
let parent = sqlite_home.parent().ok_or_else(|| {
|
|
std::io::Error::other(format!(
|
|
"cannot create a backup folder for {}",
|
|
sqlite_home.display()
|
|
))
|
|
})?;
|
|
let mut backup_dir_name = file_name(sqlite_home)?.to_os_string();
|
|
backup_dir_name.push(format!(".{BACKUP_DIR_NAME}"));
|
|
let backup_parent = parent.join(backup_dir_name);
|
|
let backup_dir = create_unique_backup_dir(backup_parent.as_path()).await?;
|
|
let backup_path = backup_dir.join(file_name(sqlite_home)?);
|
|
tokio::fs::rename(sqlite_home, backup_path.as_path()).await?;
|
|
tokio::fs::create_dir_all(sqlite_home).await?;
|
|
Ok(vec![RuntimeDbBackup {
|
|
original_path: sqlite_home.to_path_buf(),
|
|
backup_path,
|
|
}])
|
|
}
|
|
|
|
fn sqlite_paths(db_path: &Path) -> Vec<PathBuf> {
|
|
let mut wal_path = db_path.as_os_str().to_os_string();
|
|
wal_path.push("-wal");
|
|
let mut shm_path = db_path.as_os_str().to_os_string();
|
|
shm_path.push("-shm");
|
|
vec![
|
|
db_path.to_path_buf(),
|
|
PathBuf::from(wal_path),
|
|
PathBuf::from(shm_path),
|
|
]
|
|
}
|
|
|
|
async fn create_unique_backup_dir(backup_parent: &Path) -> std::io::Result<PathBuf> {
|
|
tokio::fs::create_dir_all(backup_parent).await?;
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map_or(0, |duration| duration.as_secs());
|
|
let mut sequence = 0_u32;
|
|
loop {
|
|
let backup_dir = backup_parent.join(format!("sqlite-{timestamp}-{sequence}"));
|
|
match tokio::fs::create_dir(backup_dir.as_path()).await {
|
|
Ok(()) => return Ok(backup_dir),
|
|
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
|
sequence += 1;
|
|
}
|
|
Err(err) => return Err(err),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn file_name(path: &Path) -> std::io::Result<&std::ffi::OsStr> {
|
|
path.file_name().ok_or_else(|| {
|
|
std::io::Error::other(format!(
|
|
"cannot create a backup name for {}",
|
|
path.display()
|
|
))
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "recovery_tests.rs"]
|
|
mod tests;
|