Reclaim unused SQLite log database pages in the background (#49069)

## Why

Deleting logs leaves free SQLite pages available for reuse without shrinking the database file. Reclaim excess free space while preserving a reserve for future writes.

## What changed

- Start a background incremental-vacuum worker with `StateRuntime`, enabling reclamation only for the logs database. Use a per-home file lock to select one worker and wait for it to finish during shutdown.
- Reclaim pages when at least 64 MiB and 25% of the database are free, preserving a reserve of at least 16 MiB or 10% of the database.
- Use short transactions with page and cooperative time budgets, defer work on contention, and reduce batch sizes with exponential backoff when interrupted passes make no progress.
- Record reclamation counts, durations, and reclaimed pages in telemetry.

## Testing

Add a regression test that interrupts a vacuum commit, verifies foreground writes can resume without data loss, then confirms a resumed pass shrinks the database and passes `PRAGMA integrity_check`.

GitOrigin-RevId: 8421f87b0aeef6540eda5690c84216906673c72e
This commit is contained in:
Dmitriy Kovalenko
2026-09-28 20:19:10 +00:00
committed by copyberry
parent 5a5a4aa796
commit 33a0f766a6
6 changed files with 595 additions and 6 deletions
+14 -1
View File
@@ -7,7 +7,7 @@
use crate::DbTelemetry;
use crate::migrations::repair_legacy_recency_migration_version;
use crate::runtime::RuntimeDbInitError;
use crate::runtime::recovery::RuntimeDbInitError;
use crate::telemetry;
use crate::telemetry::DbKind;
use codex_utils_absolute_path::AbsolutePathBuf;
@@ -38,6 +38,9 @@ struct RuntimeDbSpec {
kind: DbKind,
open_phase: &'static str,
migrate_phase: &'static str,
/// Opt in only after auditing writers for deferred read-to-write upgrades:
/// an intervening reclamation commit can make those fail with SQLITE_BUSY_SNAPSHOT.
background_reclamation: bool,
}
impl RuntimeDbSpec {
@@ -52,6 +55,7 @@ const STATE_DB: RuntimeDbSpec = RuntimeDbSpec {
kind: DbKind::State,
open_phase: "open_state",
migrate_phase: "migrate_state",
background_reclamation: false,
};
const LOGS_DB: RuntimeDbSpec = RuntimeDbSpec {
@@ -60,6 +64,8 @@ const LOGS_DB: RuntimeDbSpec = RuntimeDbSpec {
kind: DbKind::Logs,
open_phase: "open_logs",
migrate_phase: "migrate_logs",
// Log transactions write before reading, so they already hold the writer lock.
background_reclamation: true,
};
const GOALS_DB: RuntimeDbSpec = RuntimeDbSpec {
@@ -68,6 +74,7 @@ const GOALS_DB: RuntimeDbSpec = RuntimeDbSpec {
kind: DbKind::Goals,
open_phase: "open_goals",
migrate_phase: "migrate_goals",
background_reclamation: false,
};
const MEMORIES_DB: RuntimeDbSpec = RuntimeDbSpec {
@@ -76,11 +83,13 @@ const MEMORIES_DB: RuntimeDbSpec = RuntimeDbSpec {
kind: DbKind::Memories,
open_phase: "open_memories",
migrate_phase: "migrate_memories",
background_reclamation: false,
};
const MEMORIES_V2_DB: RuntimeDbSpec = RuntimeDbSpec {
label: "memories v2 DB",
filename: "memories_v2_1.sqlite",
background_reclamation: false,
..MEMORIES_DB
};
@@ -90,6 +99,7 @@ const QUEUE_DB: RuntimeDbSpec = RuntimeDbSpec {
kind: DbKind::Queue,
open_phase: "open_queue",
migrate_phase: "migrate_queue",
background_reclamation: false,
};
const THREAD_HISTORY_DB: RuntimeDbSpec = RuntimeDbSpec {
@@ -98,6 +108,7 @@ const THREAD_HISTORY_DB: RuntimeDbSpec = RuntimeDbSpec {
kind: DbKind::ThreadHistory,
open_phase: "open_thread_history",
migrate_phase: "migrate_thread_history",
background_reclamation: false,
};
const RUNTIME_DBS: [RuntimeDbSpec; 7] = [
@@ -114,6 +125,7 @@ const RUNTIME_DBS: [RuntimeDbSpec; 7] = [
pub struct RuntimeDbPath {
pub label: &'static str,
pub path: PathBuf,
pub(crate) background_reclamation: bool,
}
/// Resolved configuration shared by all Codex SQLite connections.
@@ -185,6 +197,7 @@ impl SqliteConfig {
.map(|spec| RuntimeDbPath {
label: spec.label,
path: spec.path(self.home()),
background_reclamation: spec.background_reclamation,
})
.collect()
}