mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
## Why A database can open successfully while containing corruption that startup previously missed. Detecting it before runtime initialization completes allows recoverable databases to be rebuilt while preserving the damaged files. ## What changed - Run `PRAGMA quick_check(1)` when opening writable databases, with a 100 ms scan budget. Share attempts across cloned `SqliteConfig` instances by file identity so repeated opens skip validation and replacement files are checked again. - Back up and rebuild runtime databases that support recovery. Report corruption without rebuilding thread history or other databases without an automatic recovery policy. Treat interrupted or locked checks as incomplete rather than confirmed corruption. - Collect startup backups across recovery retries and include their locations in the app server warning. Explain that saved conversations can restore the thread list and history, while some database-only metadata may be unavailable. - Record confirmed corruption in `codex.sqlite.corruption.count`. ## Testing Add tests for recovery policies, backup preservation, validation caching and concurrency, cancelled and interrupted checks, and locked databases. Add app server coverage for recovery warnings and saved conversation restoration, a TUI warning snapshot, and message board post preservation. GitOrigin-RevId: 43e26328888e0301def642d69bd324acee867566
81 lines
2.4 KiB
Rust
81 lines
2.4 KiB
Rust
//! Budgeted integrity checks that report confirmed corruption to database owners.
|
|
//!
|
|
//! Each file identity is checked at most once per shared SQLite configuration.
|
|
//! Attempts are recorded before validation, including failed or cancelled checks.
|
|
|
|
use file_id::FileId;
|
|
use sqlx::SqlitePool;
|
|
use std::collections::HashSet;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
use tokio::sync::Mutex;
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
pub(super) enum QuickCheckOutcome {
|
|
Complete,
|
|
Incomplete,
|
|
CorruptedNeedsFixed,
|
|
Skipped,
|
|
}
|
|
|
|
/// Keeps track of what databases were verified with quick_check() already
|
|
#[derive(Clone, Debug, Default)]
|
|
pub(super) struct SqliteQuickCheckManager(Arc<Mutex<HashSet<FileId>>>);
|
|
|
|
impl SqliteQuickCheckManager {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Returns this attempt's outcome, or `Skipped` if validation was already attempted.
|
|
pub async fn quick_check_once(
|
|
&self,
|
|
pool: &SqlitePool,
|
|
path: &Path,
|
|
budget: Duration,
|
|
) -> anyhow::Result<QuickCheckOutcome> {
|
|
let id = file_id::get_file_id(path)?;
|
|
|
|
if !self.0.lock().await.insert(id) {
|
|
// Only check each file once for this owner.
|
|
return Ok(QuickCheckOutcome::Skipped);
|
|
}
|
|
|
|
quick_check(pool, Instant::now() + budget).await
|
|
}
|
|
}
|
|
|
|
pub(super) async fn quick_check(
|
|
pool: &SqlitePool,
|
|
deadline: Instant,
|
|
) -> anyhow::Result<QuickCheckOutcome> {
|
|
let mut connection = pool.acquire().await?;
|
|
connection
|
|
.lock_handle()
|
|
.await?
|
|
.set_progress_handler(/*num_ops*/ 1_000, move || Instant::now() < deadline);
|
|
|
|
// quick_check(1) returns either "ok" or the first corruption finding
|
|
let result = sqlx::query_scalar::<_, String>("PRAGMA quick_check(1)")
|
|
.fetch_one(&mut *connection)
|
|
.await;
|
|
connection.lock_handle().await?.remove_progress_handler();
|
|
|
|
connection.return_to_pool().await;
|
|
match result.map_err(anyhow::Error::from) {
|
|
Ok(result) if result == "ok" => Ok(QuickCheckOutcome::Complete),
|
|
Ok(_) => Ok(QuickCheckOutcome::CorruptedNeedsFixed),
|
|
Err(error) if crate::is_sqlite_corruption_error(&error) => {
|
|
Ok(QuickCheckOutcome::CorruptedNeedsFixed)
|
|
}
|
|
// timeouts or other unhandled errors
|
|
Err(_) => Ok(QuickCheckOutcome::Incomplete),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "validation_tests.rs"]
|
|
mod tests;
|