use std::{fmt::Display, time::Duration};
use postgresql_embedded::{PostgreSQL, Settings};
use tokio::time;
use super::{
cleanup,
runtime::build_runtime,
worker_invoker::WorkerInvoker as ClusterWorkerInvoker,
worker_operation,
};
use crate::{
CleanupMode,
TestBootstrapSettings,
error::BootstrapResult,
observability::LOG_TARGET,
};
pub(super) struct DropContext<'a> {
pub is_managed_via_worker: bool,
pub postgres: &'a mut Option<PostgreSQL>,
pub bootstrap: &'a TestBootstrapSettings,
pub env_vars: &'a [(String, Option<String>)],
pub context: &'a str,
}
#[cfg(feature = "async-api")]
pub(super) struct InProcessCleanup<'a> {
pub cleanup_mode: CleanupMode,
pub settings: &'a Settings,
pub context: &'a str,
}
struct OwnedCleanup {
cleanup_mode: CleanupMode,
settings: Settings,
context: String,
}
impl OwnedCleanup {
fn new(cleanup_mode: CleanupMode, settings: Settings, context: &str) -> Self {
Self {
cleanup_mode,
settings,
context: context.to_owned(),
}
}
}
pub(super) fn stop_context(settings: &Settings) -> String {
let data_dir = settings.data_dir.display();
let version = settings.version.to_string();
format!("version {version}, data_dir {data_dir}")
}
#[cfg(feature = "async-api")]
pub(super) fn log_async_stop(context: &str, is_managed_via_worker: bool) {
tracing::info!(
target: LOG_TARGET,
context = %context,
worker_managed = is_managed_via_worker,
async_mode = true,
"stopping embedded postgres cluster"
);
}
#[cfg(feature = "async-api")]
pub(super) async fn stop_worker_managed_async(
bootstrap: &TestBootstrapSettings,
env_vars: &[(String, Option<String>)],
context: &str,
) -> BootstrapResult<()> {
let owned_bootstrap = bootstrap.clone();
let owned_env_vars = env_vars.to_vec();
let owned_context = context.to_owned();
tokio::task::spawn_blocking(move || {
stop_via_worker_sync(&owned_bootstrap, &owned_env_vars, &owned_context)
})
.await
.map_err(|err| {
crate::error::BootstrapError::from(color_eyre::eyre::eyre!(
"worker stop task panicked: {err}"
))
})?
}
#[cfg(feature = "async-api")]
pub(super) async fn stop_in_process_async(
postgres: PostgreSQL,
timeout: Duration,
cleanup: InProcessCleanup<'_>,
) -> BootstrapResult<()> {
let result = match time::timeout(timeout, postgres.stop()).await {
Ok(Ok(())) => Ok(()),
Ok(Err(err)) => {
warn_stop_failure(cleanup.context, &err);
Err(crate::error::BootstrapError::from(color_eyre::eyre::eyre!(
"failed to stop postgres: {err}"
)))
}
Err(_) => {
let timeout_secs = timeout.as_secs();
warn_stop_timeout(timeout_secs, cleanup.context);
Err(crate::error::BootstrapError::from(color_eyre::eyre::eyre!(
"stop timed out after {timeout_secs}s"
)))
}
};
if result.is_ok() {
cleanup::cleanup_in_process(cleanup.cleanup_mode, cleanup.settings, cleanup.context);
}
result
}
#[cfg(feature = "async-api")]
pub(super) fn stop_via_worker_sync(
bootstrap: &TestBootstrapSettings,
env_vars: &[(String, Option<String>)],
context: &str,
) -> BootstrapResult<()> {
let runtime = build_runtime()?;
stop_worker_managed_with_runtime(&runtime, bootstrap, env_vars, context)
}
fn stop_worker_managed_with_runtime(
runtime: &tokio::runtime::Runtime,
bootstrap: &TestBootstrapSettings,
env_vars: &[(String, Option<String>)],
context: &str,
) -> BootstrapResult<()> {
let invoker = ClusterWorkerInvoker::new(runtime, bootstrap, env_vars);
let result = invoker.invoke_as_root(worker_operation::WorkerOperation::Stop);
if let Err(err) = &result {
warn_stop_failure(context, err);
}
if result.is_ok() {
cleanup::cleanup_worker_managed_with_runtime(runtime, bootstrap, env_vars, context);
cleanup::cleanup_in_process(bootstrap.cleanup_mode, &bootstrap.settings, context);
}
result
}
pub(super) fn drop_sync_cluster(runtime: &tokio::runtime::Runtime, ctx: DropContext<'_>) {
let DropContext {
is_managed_via_worker,
postgres,
bootstrap,
env_vars,
context,
} = ctx;
if is_managed_via_worker {
drop(stop_worker_managed_with_runtime(
runtime, bootstrap, env_vars, context,
));
return;
}
let timeout = bootstrap.shutdown_timeout;
let timeout_secs = timeout.as_secs();
let mut is_stop_ok = false;
if let Some(pg) = postgres.take() {
let outcome = runtime.block_on(async { time::timeout(timeout, pg.stop()).await });
match outcome {
Ok(Ok(())) => is_stop_ok = true,
Ok(Err(err)) => warn_stop_failure(context, &err),
Err(_) => warn_stop_timeout(timeout_secs, context),
}
}
if is_stop_ok {
cleanup::cleanup_in_process(bootstrap.cleanup_mode, &bootstrap.settings, context);
}
}
pub(super) fn drop_async_cluster(ctx: DropContext<'_>) {
let DropContext {
is_managed_via_worker,
postgres,
bootstrap,
env_vars,
context,
} = ctx;
warn_async_drop_without_stop(context);
if is_managed_via_worker {
drop_async_worker_managed(bootstrap, env_vars, context);
} else if let Some(pg) = postgres.take() {
let cleanup =
OwnedCleanup::new(bootstrap.cleanup_mode, bootstrap.settings.clone(), context);
drop_async_in_process(bootstrap.shutdown_timeout, cleanup, pg);
}
}
fn drop_async_worker_managed(
bootstrap: &TestBootstrapSettings,
env_vars: &[(String, Option<String>)],
context: &str,
) {
let Ok(handle) = tokio::runtime::Handle::try_current() else {
error_no_runtime_for_cleanup(context);
return;
};
let owned_bootstrap = bootstrap.clone();
let owned_env_vars = env_vars.to_vec();
let owned_context = context.to_owned();
drop(handle.spawn(spawn_worker_stop_task(
owned_bootstrap,
owned_env_vars,
owned_context,
)));
}
fn drop_async_in_process(timeout: Duration, cleanup: OwnedCleanup, postgres: PostgreSQL) {
let Ok(handle) = tokio::runtime::Handle::try_current() else {
error_no_runtime_for_cleanup(&cleanup.context);
return;
};
spawn_async_cleanup(&handle, postgres, timeout, cleanup);
}
fn spawn_async_cleanup(
handle: &tokio::runtime::Handle,
postgres: PostgreSQL,
timeout: Duration,
cleanup: OwnedCleanup,
) {
drop(handle.spawn(async move {
match time::timeout(timeout, postgres.stop()).await {
Ok(Ok(())) => {
tracing::debug!(target: LOG_TARGET, "async cleanup completed successfully");
cleanup::cleanup_in_process(
cleanup.cleanup_mode,
&cleanup.settings,
&cleanup.context,
);
}
Ok(Err(err)) => {
tracing::debug!(
target: LOG_TARGET,
error = %err,
"async cleanup failed during postgres stop"
);
}
Err(_) => {
tracing::debug!(
target: LOG_TARGET,
timeout_secs = timeout.as_secs(),
"async cleanup timed out"
);
}
}
}));
}
pub(super) async fn spawn_worker_stop_task(
bootstrap: TestBootstrapSettings,
env_vars: Vec<(String, Option<String>)>,
context: String,
) {
handle_worker_stop_join(
tokio::task::spawn_blocking(move || worker_stop_sync(&bootstrap, &env_vars, &context))
.await,
);
}
fn handle_worker_stop_join(result: Result<(), tokio::task::JoinError>) {
if let Err(err) = result {
tracing::error!(
target: LOG_TARGET,
error = %err,
"worker stop task panicked during async drop"
);
}
}
fn worker_stop_sync(
bootstrap: &TestBootstrapSettings,
env_vars: &[(String, Option<String>)],
context: &str,
) {
let Ok(runtime) = build_runtime() else {
tracing::error!(
target: LOG_TARGET,
"failed to build runtime for worker stop during async drop"
);
return;
};
drop(stop_worker_managed_with_runtime(
&runtime, bootstrap, env_vars, context,
));
}
pub(super) fn warn_async_drop_without_stop(context: &str) {
tracing::warn!(
target: LOG_TARGET,
context = %context,
concat!(
"async TestCluster dropped without calling stop_async(); ",
"attempting best-effort cleanup"
)
);
}
pub(super) fn error_no_runtime_for_cleanup(context: &str) {
tracing::error!(
target: LOG_TARGET,
context = %context,
"no async runtime available for cleanup; resources may leak"
);
}
pub(super) fn warn_stop_failure(context: &str, err: &impl Display) {
tracing::warn!(
"SKIP-TEST-CLUSTER: failed to stop embedded postgres instance ({}): {}",
context,
err
);
}
pub(super) fn warn_stop_timeout(timeout_secs: u64, context: &str) {
tracing::warn!(
"SKIP-TEST-CLUSTER: stop() timed out after {timeout_secs}s ({context}); proceeding with \
drop"
);
}
#[cfg(test)]
#[path = "shutdown_tests.rs"]
mod tests;