Files
codex/codex-rs/state/src/migrations.rs
T
willwang-openai 7ae8319917 Retry SQLite operations on transient writer contention (#54369)
## Why

Competing SQLite writers can cause database opening and writes to fail with `SQLITE_BUSY`. Deferred write transactions can also encounter contention after taking a read snapshot.

## What changed

- Add up to two retries with 100 ms and 200 ms delays for SQLite busy errors during pool opening, writer admission, and standalone writes.
- Use a shared `BEGIN IMMEDIATE` helper across state, thread history, and agent message board writes to acquire the writer slot before reading.
- Preserve bound arguments when retrying standalone writes, and commit writes using `RETURNING` before exposing their results. Retry transaction admission without replaying an already-started transaction.

## Testing

Add tests using real SQLite writer contention to verify recovery after a competing writer releases its lock. Queue tests cover enqueue, update, and delete, checking preserved payloads, single revision increments, and no duplicate messages or unintended deletions.

GitOrigin-RevId: ac5bd31a87c67cc47fb385bf3fa5435a54ff70f0
2026-10-15 18:06:57 +00:00

124 lines
3.6 KiB
Rust

use std::borrow::Cow;
use crate::sqlite::SqliteWriteQueryExt;
use sqlx::migrate::Migrator;
use sqlx_sqlite::SqlitePool;
pub(crate) static STATE_MIGRATOR: Migrator = sqlx_macros::migrate!("./migrations");
pub(crate) static LOGS_MIGRATOR: Migrator = sqlx_macros::migrate!("./logs_migrations");
pub(crate) static GOALS_MIGRATOR: Migrator = sqlx_macros::migrate!("./goals_migrations");
pub(crate) static MEMORIES_MIGRATOR: Migrator = sqlx_macros::migrate!("./memory_migrations");
pub(crate) static QUEUE_MIGRATOR: Migrator = sqlx_macros::migrate!("./queue_migrations");
pub(crate) static THREAD_HISTORY_MIGRATOR: Migrator =
sqlx_macros::migrate!("./thread_history_migrations");
/// Allow an older Codex binary to open a database that has already been
/// migrated by a newer binary running in parallel.
///
/// We intentionally ignore applied migration versions that are newer than the
/// embedded migration set. Known migration versions are still validated by
/// checksum, so this only relaxes the "database is ahead of me" case.
fn runtime_migrator(base: &'static Migrator) -> Migrator {
Migrator {
migrations: Cow::Borrowed(base.migrations.as_ref()),
ignore_missing: true,
locking: base.locking,
no_tx: base.no_tx,
table_name: base.table_name.clone(),
create_schemas: base.create_schemas.clone(),
}
}
pub(crate) fn runtime_state_migrator() -> Migrator {
runtime_migrator(&STATE_MIGRATOR)
}
pub(crate) fn runtime_logs_migrator() -> Migrator {
runtime_migrator(&LOGS_MIGRATOR)
}
pub(crate) fn runtime_goals_migrator() -> Migrator {
runtime_migrator(&GOALS_MIGRATOR)
}
pub(crate) fn runtime_memories_migrator() -> Migrator {
runtime_migrator(&MEMORIES_MIGRATOR)
}
pub(crate) fn runtime_queue_migrator() -> Migrator {
runtime_migrator(&QUEUE_MIGRATOR)
}
// The paginated history projector will call this when it takes ownership of opening the database.
#[allow(dead_code)]
pub(crate) fn runtime_thread_history_migrator() -> Migrator {
runtime_migrator(&THREAD_HISTORY_MIGRATOR)
}
pub(crate) async fn repair_legacy_recency_migration_version(
pool: &SqlitePool,
migrator: &Migrator,
) -> anyhow::Result<()> {
let Some(recency_migration) = migrator
.migrations
.iter()
.find(|migration| migration.version == 39)
else {
return Ok(());
};
let migrations_table_exists = sqlx::query_scalar::<_, i64>(
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_sqlx_migrations'",
)
.fetch_optional(pool)
.await?
.is_some();
if !migrations_table_exists {
return Ok(());
}
let legacy_recency_needs_repair = sqlx::query_scalar::<_, i64>(
r#"
SELECT 1
FROM _sqlx_migrations
WHERE version = ?
AND checksum = ?
AND NOT EXISTS (
SELECT 1 FROM _sqlx_migrations WHERE version = ?
)
"#,
)
.bind(38_i64)
.bind(recency_migration.checksum.as_ref())
.bind(recency_migration.version)
.fetch_optional(pool)
.await?
.is_some();
if !legacy_recency_needs_repair {
return Ok(());
}
sqlx::query(
r#"
UPDATE _sqlx_migrations
SET version = ?, description = ?
WHERE version = ?
AND checksum = ?
AND NOT EXISTS (
SELECT 1 FROM _sqlx_migrations WHERE version = ?
)
"#,
)
.bind(recency_migration.version)
.bind(recency_migration.description.as_ref())
.bind(38_i64)
.bind(recency_migration.checksum.as_ref())
.bind(recency_migration.version)
.execute_with_busy_retry(pool)
.await?;
Ok(())
}
#[cfg(test)]
#[path = "migrations_tests.rs"]
mod tests;