add option to migrate database in-place without copying

I originally didn't plan to include anything like this, but a few people
in #grapevine:computer.surgery pointed out use-cases where it might be
desirable:

 - a server that doesn't have enough room to store two full copies of
   the db. Transferring a copy over the network to back up is viable.
 - making a reflinked copy on a CoW filesystem (we could support this in
   the tool, but don't currently)
 - a server with some other backup mechanism available like snapshotting
   the entire filesystem
This commit is contained in:
Benjamin Lee 2024-09-14 01:41:04 -07:00
parent b93d4f471c
commit 3226d3a9eb
No known key found for this signature in database
GPG key ID: FB9624E2885D55A4
3 changed files with 55 additions and 7 deletions

View file

@ -96,12 +96,36 @@ pub(crate) struct MigrateDbArgs {
pub(crate) to: DbMigrationTarget,
/// Path to read database from.
#[clap(long = "in", short)]
pub(crate) in_path: PathBuf,
#[clap(long = "in", short, required_unless_present("inplace_path"))]
pub(crate) in_path: Option<PathBuf>,
/// Path to write migrated database to.
#[clap(long = "out", short)]
pub(crate) out_path: PathBuf,
#[clap(long = "out", short, required_unless_present("inplace_path"))]
pub(crate) out_path: Option<PathBuf>,
/// Path to modify an existing database in-place, instead of copying before
/// migrating.
///
/// Note that even a successful migration may lose data, because some parts
/// of the schema present in the initial database may not exist in the
/// target version. Because of this, it's very important to have a
/// backup of the initial database when migrating. The preferred way to
/// do this is with the --in and --out flags, which ensure that the
/// original database path is left unmodified. In some situations, it
/// may be possible to take a backup some other way (transferring it
/// over the network, for example), but copying the files locally is
/// undesirable. In this case, setting the --i-have-tested-my-backups
/// flag enables the use of --inplace to modify the database without
/// copying to a new location first.
#[clap(long = "inplace", conflicts_with_all(["in_path", "out_path"]))]
pub(crate) inplace_path: Option<PathBuf>,
/// Set if you have tested your backups, to enable use of the --inplace
/// flag.
///
/// See the documentation of --inplace for more details.
#[clap(long)]
pub(crate) i_have_tested_my_backups: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]

View file

@ -34,19 +34,33 @@ pub(crate) async fn run(
) -> Result<(), error::MigrateDbCommand> {
use error::MigrateDbCommand as Error;
let db_path = if let Some(path) = args.inplace_path {
if !args.i_have_tested_my_backups {
return Err(Error::InplaceUnconfirmed);
}
path
} else {
let in_path = args
.in_path
.expect("in_path should be required if inplace_path is unset");
let out_path = args
.out_path
.expect("out_path should be required if inplace_path is unset");
copy_dir(&in_path, &out_path).await.map_err(Error::Copy)?;
out_path
};
let mut config = config::load(args.config.config.as_ref()).await?;
// mutating the config like this is ugly, but difficult to avoid. Currently
// the database is very tightly coupled with service code, which reads the
// path only from the config.
args.out_path
db_path
.to_str()
.ok_or(Error::InvalidUnicodeOutPath)?
.clone_into(&mut config.database.path);
let (_guard, reload_handles) = observability::init(&config)?;
copy_dir(&args.in_path, &args.out_path).await.map_err(Error::Copy)?;
let db = Box::leak(Box::new(
KeyValueDatabase::load_or_create(&config).map_err(Error::LoadDb)?,
));

View file

@ -97,6 +97,16 @@ pub(crate) enum MigrateDbCommand {
#[error("output path is not valid unicode")]
InvalidUnicodeOutPath,
#[error(
"migrating a database may lose data even if the migration is \
successful. Because of this, it is very important to ensure you have \
a working backup when using the --inplace flag. If you have a tested \
backup, set the --i-have-tested-my-backups flag to enable use of \
--inplace. Alternatively, use --from and --to instead of --inplace \
to ensure the original database is preserved."
)]
InplaceUnconfirmed,
#[error("failed to copy existing database directory")]
Copy(#[source] CopyDir),