add subcmd to repair some persistent state

This commit is contained in:
Charles Hall 2024-10-10 12:02:54 -07:00
parent 5be1e20eb4
commit 07d05fa1f9
No known key found for this signature in database
GPG key ID: 7B8E0645816E07CF
3 changed files with 131 additions and 0 deletions

View file

@ -13,6 +13,7 @@ use crate::{
};
mod check_config;
mod repair;
mod serve;
/// Command line arguments
@ -33,6 +34,21 @@ pub(crate) enum Command {
/// Check the configuration file for syntax and semantic errors.
CheckConfig(CheckConfigArgs),
/// Repair (some) persistent state (MAKE A BACKUP FIRST!)
///
/// This subcommand is idempotent; if it exits zero, it can safely be run
/// again.
///
/// If this subcommand exits nonzero, restore from a backup. Some causes
/// of failure can be fixed by the user and then this subcommand can be
/// attempted again, others may be a bug in this subcommand.
///
/// Currently fixes the following issues:
///
/// * <https://gitlab.computer.surgery/matrix/grapevine/-/issues/27>
/// * <https://gitlab.computer.surgery/matrix/grapevine/-/issues/54>
Repair(RepairArgs),
}
#[derive(clap::Args)]
@ -88,6 +104,15 @@ pub(crate) struct ServeArgs {
pub(crate) config: ConfigArg,
}
#[derive(clap::Args)]
pub(crate) struct RepairArgs {
#[clap(flatten)]
observability: ObservabilityArgs,
#[clap(flatten)]
pub(crate) config: ConfigArg,
}
impl Args {
pub(crate) async fn run(self) -> Result<(), error::Main> {
if let Some((format, filter)) = self.command.cli_observability_args() {
@ -99,6 +124,7 @@ impl Args {
Command::CheckConfig(args) => {
check_config::run(args.config).await?;
}
Command::Repair(args) => repair::run(args).await?,
}
Ok(())
}
@ -113,6 +139,10 @@ impl Command {
args.observability.log_format,
args.observability.log_filter.clone(),
)),
Command::Repair(args) => Some((
args.observability.log_format,
args.observability.log_filter.clone(),
)),
Command::Serve(_) => None,
}
}

76
src/cli/repair.rs Normal file
View file

@ -0,0 +1,76 @@
#![warn(clippy::missing_docs_in_private_items)]
//! Implementation of the `repair` subcommand
use std::error::Error;
use tracing as t;
use super::RepairArgs;
use crate::{config, database::KeyValueDatabase, error, services, Services};
/// Subcommand entrypoint
pub(crate) async fn run(args: RepairArgs) -> Result<(), error::RepairCommand> {
use error::RepairCommand as Error;
t::info!("Repairing persistent state");
let config =
config::load(args.config.config).await.map_err(Error::Config)?;
let db = Box::leak(Box::new(
KeyValueDatabase::load_or_create(&config).map_err(Error::Database)?,
));
Services::build(db, config, None)
.map_err(Error::InitializeServices)?
.install();
services().globals.err_if_server_name_changed()?;
db.apply_migrations().await.map_err(Error::Database)?;
repair_roomuserid_joined(db)
.map_err(|e| Error::Repair("roomuserid_joined", e))?;
t::info!("Done");
Ok(())
}
/// Repair the `roomuserid_joined` map
#[t::instrument(skip(db))]
fn repair_roomuserid_joined(
db: &KeyValueDatabase,
) -> Result<(), Box<dyn Error>> {
t::info!(length = db.roomuserid_joined.iter().count(), "Old map length");
db.roomuserid_joined.clear()?;
for (k, _) in db.userroomid_joined.iter() {
let mut segments = k.split(|x| *x == 0xFF);
let Some(user_id_bytes) = segments.next() else {
t::warn!("No user ID bytes in key, skipping this pair");
continue;
};
let Some(room_id_bytes) = segments.next() else {
t::warn!("No room ID bytes in key, skipping this pair");
continue;
};
let count = segments.count();
if count != 0 {
t::warn!(count, "Extra segments in key, ignoring them");
}
let key = [room_id_bytes, user_id_bytes].join(&0xFF);
db.roomuserid_joined.insert(&key, &[])?;
}
t::info!(length = db.roomuserid_joined.iter().count(), "New map length");
Ok(())
}

View file

@ -48,6 +48,9 @@ pub(crate) enum Main {
#[error(transparent)]
CheckConfigCommand(#[from] CheckConfigCommand),
#[error(transparent)]
RepairCommand(#[from] RepairCommand),
}
/// Errors returned from the `serve` CLI subcommand.
@ -85,6 +88,28 @@ pub(crate) enum CheckConfigCommand {
Config(#[from] Config),
}
/// Errors returned from the `repair` CLI subcommand.
// Missing docs are allowed here since that kind of information should be
// encoded in the error messages themselves anyway.
#[allow(missing_docs)]
#[derive(Error, Debug)]
pub(crate) enum RepairCommand {
#[error("failed to load configuration")]
Config(#[from] Config),
#[error("failed to load or create the database")]
Database(#[source] crate::utils::error::Error),
#[error("failed to initialize services")]
InitializeServices(#[source] crate::utils::error::Error),
#[error("`server_name` change check failed")]
ServerNameChanged(#[from] ServerNameChanged),
#[error("repair \"{0}\" failed, restoring from a backup is recommended")]
Repair(&'static str, #[source] Box<dyn std::error::Error>),
}
/// Error generated if `server_name` has changed or if checking this failed
// Missing docs are allowed here since that kind of information should be
// encoded in the error messages themselves anyway.