mirror of
https://github.com/openai/codex.git
synced 2026-10-28 18:58:46 +00:00
## Why `codex migrate-nfs-db` reported a successful migration when the database directory existed but contained no databases. ## What changed Count databases that were migrated or already use DELETE journal mode. If none are found, print `No databases found.` instead of the migration success message. ## Testing Extend the CLI integration test to cover an empty directory, a failure on a corrupt logs database after an earlier conversion, and a successful rerun that recognizes the converted database as already in DELETE mode. GitOrigin-RevId: 16b109645925164f3af5d35a909417872cfa004f
118 lines
4.7 KiB
Rust
118 lines
4.7 KiB
Rust
//! Confirms explicit database migration and reports the shared migrator's results.
|
|
//! Other SQLite clients must remain stopped during replacement, and NFS must enforce
|
|
//! cross-client POSIX locks.
|
|
|
|
use anyhow::Context;
|
|
use anyhow::ensure;
|
|
use clap::Parser;
|
|
use codex_config::LoaderOverrides;
|
|
use codex_core::config::ConfigBuilder;
|
|
use codex_state::NfsDbMigration;
|
|
use codex_state::NfsDbMigrationOutcome;
|
|
use codex_utils_cli::CliConfigOverrides;
|
|
use owo_colors::OwoColorize;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(
|
|
after_help = "Stop SQLite clients on every host and keep them stopped through cutover.\nThe NFS mount must support cross-client POSIX locks (no nolock/local_lock=all).\nThis command replaces configured databases in place and retains their WAL backups."
|
|
)]
|
|
pub(crate) struct MigrateNfsDbCommand {
|
|
/// Use a local directory with space for the database and WAL files.
|
|
/// The default is the system temporary directory.
|
|
#[arg(long, default_value_os_t = std::env::temp_dir())]
|
|
staging_dir: PathBuf,
|
|
}
|
|
|
|
pub(crate) async fn run(
|
|
command: MigrateNfsDbCommand,
|
|
config_overrides: CliConfigOverrides,
|
|
loader_overrides: LoaderOverrides,
|
|
) -> anyhow::Result<()> {
|
|
ensure!(
|
|
cfg!(unix),
|
|
"migrate-nfs-db currently requires Unix filesystem locking and directory fsync"
|
|
);
|
|
let warning = "WARNING: This operation is unsafe under concurrency.";
|
|
if supports_color::on(supports_color::Stream::Stderr).is_some() {
|
|
eprintln!("{}", warning.red().bold());
|
|
} else {
|
|
eprintln!("{warning}");
|
|
}
|
|
eprintln!(
|
|
"Stop all Codex instances on every host that uses these databases.\nKeep them stopped until the migration completes successfully."
|
|
);
|
|
let config = ConfigBuilder::default()
|
|
.cli_overrides(
|
|
config_overrides
|
|
.parse_overrides()
|
|
.map_err(anyhow::Error::msg)?,
|
|
)
|
|
.loader_overrides(loader_overrides)
|
|
.build()
|
|
.await?;
|
|
// Check the merged config for an explicit log directory.
|
|
let effective_config = config.config_layer_stack.effective_config();
|
|
let skip_logs = effective_config.get("log_dir").is_some();
|
|
let sqlite = config.sqlite_config();
|
|
if !sqlite.home().try_exists()? {
|
|
println!("No databases found.");
|
|
return Ok(());
|
|
}
|
|
let mut needs_permission_confirmation = false;
|
|
for database in sqlite.runtime_db_paths() {
|
|
if skip_logs && database.path == sqlite.logs_db_path() {
|
|
continue;
|
|
}
|
|
needs_permission_confirmation |=
|
|
NfsDbMigration::needs_permission_confirmation(&database.path)?;
|
|
}
|
|
if needs_permission_confirmation {
|
|
eprintln!(
|
|
"Some database files have permissions other than 0600.\nMigration preserves file mode bits, including group and other access."
|
|
);
|
|
}
|
|
eprintln!(
|
|
"Replacement files belong to you; the original owner, group, and extended ACLs are not copied.\nThis can remove another user's access even when file permissions are 0600.\nOriginal files remain in a private backup directory.\nYou need write access to the database directory and its migration lock."
|
|
);
|
|
let prompt = if needs_permission_confirmation {
|
|
"Migrate databases, including files with permissions other than 0600? [y/N]:"
|
|
} else {
|
|
"Continue? [y/N]:"
|
|
};
|
|
ensure!(
|
|
crate::confirm(prompt)?,
|
|
"Migration cancelled. No databases were changed."
|
|
);
|
|
let mut migration = NfsDbMigration::lock(sqlite.home())?;
|
|
let mut databases_found = 0;
|
|
for database in sqlite.runtime_db_paths() {
|
|
if skip_logs && database.path == sqlite.logs_db_path() {
|
|
println!("Skipped logs database: log_dir is configured.");
|
|
continue;
|
|
}
|
|
let outcome = migration.migrate(&database.path, &command.staging_dir).await.with_context(|| format!(
|
|
"migration failed for {}. Keep all Codex instances stopped. Correct the error and rerun codex migrate-nfs-db",
|
|
database.path.display()
|
|
))?;
|
|
match outcome {
|
|
NfsDbMigrationOutcome::Missing => continue,
|
|
NfsDbMigrationOutcome::AlreadyDelete => {
|
|
println!("Already in DELETE mode: {}", database.path.display())
|
|
}
|
|
NfsDbMigrationOutcome::Migrated { backup_directory } => println!(
|
|
"Migrated database: {}\nWAL backup: {}",
|
|
database.path.display(),
|
|
backup_directory.display()
|
|
),
|
|
}
|
|
databases_found += 1;
|
|
}
|
|
if databases_found == 0 {
|
|
println!("No databases found.");
|
|
return Ok(());
|
|
}
|
|
println!("Migration completed successfully. You can restart Codex.");
|
|
Ok(())
|
|
}
|