use std::{
any::Any,
panic::{AssertUnwindSafe, catch_unwind, resume_unwind},
sync::{Arc, Mutex, OnceLock},
};
use color_eyre::eyre::eyre;
use dashmap::DashMap;
use crate::error::{BootstrapError, BootstrapResult};
pub(super) trait TemplateLockOps {
fn with_template_lock<R>(&self, template_name: &str, operation: impl FnOnce() -> R) -> R;
}
pub(super) struct StdTemplateLocks;
static TEMPLATE_LOCKS: OnceLock<DashMap<String, Arc<Mutex<()>>>> = OnceLock::new();
impl TemplateLockOps for StdTemplateLocks {
fn with_template_lock<R>(&self, template_name: &str, operation: impl FnOnce() -> R) -> R {
let locks = TEMPLATE_LOCKS.get_or_init(DashMap::new);
let lock = locks
.entry(template_name.to_owned())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone();
let _guard = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
operation()
}
}
pub(super) static STD_TEMPLATE_LOCKS: StdTemplateLocks = StdTemplateLocks;
pub(super) struct TemplateCreationOps<Exists, Create, Drop, Setup> {
pub(super) database_exists: Exists,
pub(super) create_database: Create,
pub(super) drop_database: Drop,
pub(super) setup_fn: Setup,
}
pub(super) fn ensure_template_exists_with_lock<L, Exists, Create, Drop, Setup>(
locks: &L,
template_name: &str,
ops: TemplateCreationOps<Exists, Create, Drop, Setup>,
) -> BootstrapResult<()>
where
L: TemplateLockOps,
Exists: FnMut() -> BootstrapResult<bool>,
Create: FnMut() -> BootstrapResult<()>,
Drop: FnMut() -> BootstrapResult<()>,
Setup: FnOnce() -> BootstrapResult<()>,
{
let TemplateCreationOps {
mut database_exists,
mut create_database,
mut drop_database,
setup_fn,
} = ops;
locks.with_template_lock(template_name, || {
if database_exists()? {
return Ok(());
}
create_and_setup_template(
template_name,
&mut create_database,
&mut drop_database,
setup_fn,
)
})
}
fn create_and_setup_template<Create, Drop, Setup>(
template_name: &str,
create_database: &mut Create,
drop_database: &mut Drop,
setup_fn: Setup,
) -> BootstrapResult<()>
where
Create: FnMut() -> BootstrapResult<()>,
Drop: FnMut() -> BootstrapResult<()>,
Setup: FnOnce() -> BootstrapResult<()>,
{
create_database()?;
setup_template_or_rollback(template_name, setup_fn, drop_database)
}
fn setup_template_or_rollback<Drop, Setup>(
template_name: &str,
setup_fn: Setup,
drop_database: &mut Drop,
) -> BootstrapResult<()>
where
Drop: FnMut() -> BootstrapResult<()>,
Setup: FnOnce() -> BootstrapResult<()>,
{
match catch_unwind(AssertUnwindSafe(setup_fn)) {
Ok(Ok(())) => Ok(()),
Ok(Err(setup_error)) => {
rollback_after_setup_error(template_name, setup_error, drop_database)
}
Err(payload) => resume_or_report_rollback_failure(template_name, payload, drop_database),
}
}
fn rollback_after_setup_error<Drop>(
template_name: &str,
setup_error: BootstrapError,
drop_database: &mut Drop,
) -> BootstrapResult<()>
where
Drop: FnMut() -> BootstrapResult<()>,
{
match rollback_and_log(
template_name,
drop_database,
"template setup failed and rollback completed",
false,
) {
Ok(()) => Err(setup_error),
Err(rollback_error) => Err(template_setup_rollback_error(setup_error, rollback_error)),
}
}
fn resume_or_report_rollback_failure<Drop>(
template_name: &str,
payload: Box<dyn Any + Send>,
drop_database: &mut Drop,
) -> BootstrapResult<()>
where
Drop: FnMut() -> BootstrapResult<()>,
{
match rollback_and_log(
template_name,
drop_database,
"template setup panicked and rollback completed",
true,
) {
Ok(()) => resume_unwind(payload),
Err(rollback_error) => Err(template_setup_panic_rollback_error(
payload.as_ref(),
rollback_error,
)),
}
}
fn rollback_and_log<Drop>(
template_name: &str,
drop_database: &mut Drop,
message: &'static str,
is_panic_path: bool,
) -> Result<(), BootstrapError>
where
Drop: FnMut() -> BootstrapResult<()>,
{
let (success_panic_action, failure_panic_action) = if is_panic_path {
(Some("resume_unwind"), Some("converted_to_error"))
} else {
(None, None)
};
match drop_database() {
Ok(()) => {
log_template_rollback(template_name, true, success_panic_action, message);
Ok(())
}
Err(rollback_error) => {
log_template_rollback(template_name, false, failure_panic_action, message);
Err(rollback_error)
}
}
}
fn log_template_rollback(
template_name: &str,
rollback_succeeded: bool,
panic_action: Option<&'static str>,
message: &'static str,
) {
tracing::warn!(
target: crate::observability::LOG_TARGET,
template_name,
rollback_succeeded,
panic_action,
"{message}"
);
}
fn panic_payload_message(payload: &(dyn Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
return (*message).to_owned();
}
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
"non-string panic payload".to_owned()
}
fn template_setup_panic_rollback_error(
payload: &(dyn Any + Send),
rollback_error: BootstrapError,
) -> BootstrapError {
let rollback_message = rollback_error.into_report().to_string();
eyre!(
"template setup panicked ({}) and rollback failed: {rollback_message}",
panic_payload_message(payload)
)
.into()
}
fn template_setup_rollback_error(
setup_error: BootstrapError,
rollback_error: BootstrapError,
) -> BootstrapError {
let setup_message = setup_error.to_string();
let rollback_message = rollback_error.into_report().to_string();
setup_error
.into_report()
.wrap_err(format!(
"template rollback failed after setup error '{setup_message}': {rollback_message}"
))
.into()
}
#[cfg(test)]
mod tests;