mirror of
https://github.com/openai/codex.git
synced 2026-09-09 15:58:47 +00:00
## What changed - Add a storage-neutral `QueueStore` interface backed by a dedicated SQLite database for ordered, thread-scoped user submissions. - Support enqueueing, paginated listing, editing, deletion, and atomic reordering, with a limit of 100 pending items per thread. - Remove queued submissions when their thread is deleted. ## Testing - Cover concurrent inserts, ordering and pagination, edits, deletion, cross-thread isolation, queue limits, and thread cleanup. GitOrigin-RevId: 0a8e95ac71137afc6f3a039d4b5fc8603110c891
306 lines
8.9 KiB
Rust
306 lines
8.9 KiB
Rust
//! Shared SQLite connection configuration.
|
|
|
|
#![expect(
|
|
clippy::disallowed_methods,
|
|
reason = "this is the centralized SQLite connection shim"
|
|
)]
|
|
|
|
use crate::DbTelemetry;
|
|
use crate::migrations::repair_legacy_recency_migration_version;
|
|
use crate::runtime::RuntimeDbInitError;
|
|
use crate::telemetry;
|
|
use crate::telemetry::DbKind;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use log::LevelFilter;
|
|
use sqlx::ConnectOptions;
|
|
use sqlx::Error;
|
|
use sqlx::SqlitePool;
|
|
use sqlx::migrate::Migrator;
|
|
use sqlx::sqlite::SqliteAutoVacuum;
|
|
use sqlx::sqlite::SqliteConnectOptions;
|
|
use sqlx::sqlite::SqliteJournalMode;
|
|
use sqlx::sqlite::SqlitePoolOptions;
|
|
use sqlx::sqlite::SqliteSynchronous;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
use std::time::Instant;
|
|
|
|
const LOGS_DB_FILENAME: &str = "logs_2.sqlite";
|
|
const GOALS_DB_FILENAME: &str = "goals_1.sqlite";
|
|
const MEMORIES_DB_FILENAME: &str = "memories_1.sqlite";
|
|
const QUEUE_DB_FILENAME: &str = "queue_1.sqlite";
|
|
const STATE_DB_FILENAME: &str = "state_5.sqlite";
|
|
const THREAD_HISTORY_DB_FILENAME: &str = "thread_history_1.sqlite";
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct RuntimeDbSpec {
|
|
label: &'static str,
|
|
filename: &'static str,
|
|
kind: DbKind,
|
|
open_phase: &'static str,
|
|
migrate_phase: &'static str,
|
|
}
|
|
|
|
impl RuntimeDbSpec {
|
|
fn path(self, codex_home: &Path) -> PathBuf {
|
|
codex_home.join(self.filename)
|
|
}
|
|
}
|
|
|
|
const STATE_DB: RuntimeDbSpec = RuntimeDbSpec {
|
|
label: "state DB",
|
|
filename: STATE_DB_FILENAME,
|
|
kind: DbKind::State,
|
|
open_phase: "open_state",
|
|
migrate_phase: "migrate_state",
|
|
};
|
|
|
|
const LOGS_DB: RuntimeDbSpec = RuntimeDbSpec {
|
|
label: "log DB",
|
|
filename: LOGS_DB_FILENAME,
|
|
kind: DbKind::Logs,
|
|
open_phase: "open_logs",
|
|
migrate_phase: "migrate_logs",
|
|
};
|
|
|
|
const GOALS_DB: RuntimeDbSpec = RuntimeDbSpec {
|
|
label: "goals DB",
|
|
filename: GOALS_DB_FILENAME,
|
|
kind: DbKind::Goals,
|
|
open_phase: "open_goals",
|
|
migrate_phase: "migrate_goals",
|
|
};
|
|
|
|
const MEMORIES_DB: RuntimeDbSpec = RuntimeDbSpec {
|
|
label: "memories DB",
|
|
filename: MEMORIES_DB_FILENAME,
|
|
kind: DbKind::Memories,
|
|
open_phase: "open_memories",
|
|
migrate_phase: "migrate_memories",
|
|
};
|
|
|
|
const QUEUE_DB: RuntimeDbSpec = RuntimeDbSpec {
|
|
label: "queue DB",
|
|
filename: QUEUE_DB_FILENAME,
|
|
kind: DbKind::Queue,
|
|
open_phase: "open_queue",
|
|
migrate_phase: "migrate_queue",
|
|
};
|
|
|
|
const THREAD_HISTORY_DB: RuntimeDbSpec = RuntimeDbSpec {
|
|
label: "thread history DB",
|
|
filename: THREAD_HISTORY_DB_FILENAME,
|
|
kind: DbKind::ThreadHistory,
|
|
open_phase: "open_thread_history",
|
|
migrate_phase: "migrate_thread_history",
|
|
};
|
|
|
|
const RUNTIME_DBS: [RuntimeDbSpec; 6] = [
|
|
STATE_DB,
|
|
LOGS_DB,
|
|
GOALS_DB,
|
|
MEMORIES_DB,
|
|
QUEUE_DB,
|
|
THREAD_HISTORY_DB,
|
|
];
|
|
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct RuntimeDbPath {
|
|
pub label: &'static str,
|
|
pub path: PathBuf,
|
|
}
|
|
|
|
/// Resolved configuration shared by all Codex SQLite connections.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct SqliteConfig {
|
|
sqlite_home: AbsolutePathBuf,
|
|
}
|
|
|
|
impl SqliteConfig {
|
|
pub fn from_sqlite_home(sqlite_home: AbsolutePathBuf) -> Self {
|
|
Self { sqlite_home }
|
|
}
|
|
|
|
pub fn new_for_testing(sqlite_home: AbsolutePathBuf) -> Self {
|
|
Self::from_sqlite_home(sqlite_home)
|
|
}
|
|
|
|
pub fn home(&self) -> &Path {
|
|
self.sqlite_home.as_path()
|
|
}
|
|
|
|
/// Return the path to the primary state database.
|
|
pub fn state_db_path(&self) -> PathBuf {
|
|
STATE_DB.path(self.home())
|
|
}
|
|
|
|
/// Return the path to the logs database.
|
|
pub fn logs_db_path(&self) -> PathBuf {
|
|
LOGS_DB.path(self.home())
|
|
}
|
|
|
|
/// Return the path to the goals database.
|
|
pub fn goals_db_path(&self) -> PathBuf {
|
|
GOALS_DB.path(self.home())
|
|
}
|
|
|
|
/// Return the path to the memories database.
|
|
pub fn memories_db_path(&self) -> PathBuf {
|
|
MEMORIES_DB.path(self.home())
|
|
}
|
|
|
|
/// Return the path to the durable user-message queue database.
|
|
pub fn queue_db_path(&self) -> PathBuf {
|
|
QUEUE_DB.path(self.home())
|
|
}
|
|
|
|
/// Return the path to the paginated thread-history database.
|
|
pub fn thread_history_db_path(&self) -> PathBuf {
|
|
THREAD_HISTORY_DB.path(self.home())
|
|
}
|
|
|
|
/// Return the paths to every database managed by the state runtime.
|
|
pub fn runtime_db_paths(&self) -> Vec<RuntimeDbPath> {
|
|
RUNTIME_DBS
|
|
.iter()
|
|
.map(|spec| RuntimeDbPath {
|
|
label: spec.label,
|
|
path: spec.path(self.home()),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub(super) async fn open_state_db(
|
|
&self,
|
|
migrator: &Migrator,
|
|
telemetry_override: Option<&dyn DbTelemetry>,
|
|
) -> anyhow::Result<SqlitePool> {
|
|
// New state DBs should use incremental auto-vacuum, but retrofitting an
|
|
// existing DB requires a full VACUUM. Do not attempt that during process
|
|
// startup: it is maintenance work that can contend with foreground writers.
|
|
self.open_runtime_db(STATE_DB, migrator, telemetry_override)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn open_logs_db(
|
|
&self,
|
|
migrator: &Migrator,
|
|
telemetry_override: Option<&dyn DbTelemetry>,
|
|
) -> anyhow::Result<SqlitePool> {
|
|
self.open_runtime_db(LOGS_DB, migrator, telemetry_override)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn open_goals_db(
|
|
&self,
|
|
migrator: &Migrator,
|
|
telemetry_override: Option<&dyn DbTelemetry>,
|
|
) -> anyhow::Result<SqlitePool> {
|
|
self.open_runtime_db(GOALS_DB, migrator, telemetry_override)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn open_memories_db(
|
|
&self,
|
|
migrator: &Migrator,
|
|
telemetry_override: Option<&dyn DbTelemetry>,
|
|
) -> anyhow::Result<SqlitePool> {
|
|
self.open_runtime_db(MEMORIES_DB, migrator, telemetry_override)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn open_queue_db(
|
|
&self,
|
|
migrator: &Migrator,
|
|
telemetry_override: Option<&dyn DbTelemetry>,
|
|
) -> anyhow::Result<SqlitePool> {
|
|
self.open_runtime_db(QUEUE_DB, migrator, telemetry_override)
|
|
.await
|
|
}
|
|
|
|
pub(super) async fn open_thread_history_db(
|
|
&self,
|
|
migrator: &Migrator,
|
|
telemetry_override: Option<&dyn DbTelemetry>,
|
|
) -> anyhow::Result<SqlitePool> {
|
|
self.open_runtime_db(THREAD_HISTORY_DB, migrator, telemetry_override)
|
|
.await
|
|
}
|
|
|
|
async fn open_runtime_db(
|
|
&self,
|
|
spec: RuntimeDbSpec,
|
|
migrator: &Migrator,
|
|
telemetry_override: Option<&dyn DbTelemetry>,
|
|
) -> anyhow::Result<SqlitePool> {
|
|
let path = spec.path(self.home());
|
|
let started = Instant::now();
|
|
let pool_result = self
|
|
.open_read_write_pool(&path)
|
|
.await
|
|
.map_err(anyhow::Error::from);
|
|
telemetry::record_init_result(
|
|
telemetry_override,
|
|
spec.kind,
|
|
spec.open_phase,
|
|
started.elapsed(),
|
|
&pool_result,
|
|
);
|
|
let pool = pool_result.map_err(|source| {
|
|
RuntimeDbInitError::new(spec.label, "open", path.as_path(), source)
|
|
})?;
|
|
let started = Instant::now();
|
|
let migrate_result = async {
|
|
if matches!(spec.kind, DbKind::State) {
|
|
repair_legacy_recency_migration_version(&pool, migrator).await?;
|
|
}
|
|
migrator.run(&pool).await.map_err(anyhow::Error::from)
|
|
}
|
|
.await;
|
|
telemetry::record_init_result(
|
|
telemetry_override,
|
|
spec.kind,
|
|
spec.migrate_phase,
|
|
started.elapsed(),
|
|
&migrate_result,
|
|
);
|
|
if let Err(source) = migrate_result {
|
|
pool.close().await;
|
|
return Err(
|
|
RuntimeDbInitError::new(spec.label, "migrate", path.as_path(), source).into(),
|
|
);
|
|
}
|
|
Ok(pool)
|
|
}
|
|
|
|
/// Open a writable Codex SQLite database, creating it if necessary.
|
|
pub async fn open_read_write_pool(&self, path: &Path) -> Result<SqlitePool, Error> {
|
|
let options = SqliteConnectOptions::new()
|
|
.filename(path)
|
|
.create_if_missing(true)
|
|
.journal_mode(SqliteJournalMode::Wal)
|
|
.synchronous(SqliteSynchronous::Normal)
|
|
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
|
.busy_timeout(Duration::from_secs(5))
|
|
.log_statements(LevelFilter::Off);
|
|
SqlitePoolOptions::new()
|
|
.max_connections(5)
|
|
.connect_with(options)
|
|
.await
|
|
}
|
|
|
|
/// Open an existing Codex SQLite database without creating or modifying it.
|
|
pub async fn open_read_only_pool(&self, path: &Path) -> Result<SqlitePool, Error> {
|
|
let options = SqliteConnectOptions::new()
|
|
.filename(path)
|
|
.create_if_missing(false)
|
|
.read_only(true)
|
|
.log_statements(LevelFilter::Off);
|
|
SqlitePoolOptions::new()
|
|
.max_connections(1)
|
|
.connect_with(options)
|
|
.await
|
|
}
|
|
}
|