mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
## 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
124 lines
3.6 KiB
Rust
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;
|