use std::future::Future;
use color_eyre::eyre::{Context, eyre};
use tokio::runtime::Runtime;
use tracing::{error, info, info_span};
use super::{WorkerOperation, panic_utils::nested_runtime_thread_panic};
#[cfg(all(unix, privileged_unix_platform))]
use crate::worker_process::{self, WorkerRequest, WorkerRequestArgs};
use crate::{
ExecutionMode,
ExecutionPrivileges,
TestBootstrapSettings,
bootstrap::{root_privilege_drop_supported, unsupported_root_privilege_drop_error},
error::{BootstrapError, BootstrapResult},
observability::LOG_TARGET,
};
fn create_lifecycle_span(
operation: WorkerOperation,
bootstrap: &TestBootstrapSettings,
async_mode: bool,
) -> tracing::Span {
let span = info_span!(
target: LOG_TARGET,
"lifecycle_operation",
operation = operation.as_str(),
privileges = ?bootstrap.privileges,
mode = ?bootstrap.execution_mode,
async_mode = tracing::field::Empty
);
if async_mode {
span.record("async_mode", true);
}
span
}
fn execute_root_operation(
bootstrap: &TestBootstrapSettings,
env_vars: &[(String, Option<String>)],
operation: WorkerOperation,
) -> BootstrapResult<()> {
#[cfg(any(test, feature = "cluster-unit-tests"))]
{
let hook_slot = crate::test_support::run_root_operation_hook()
.lock()
.unwrap_or_else(|poison| poison.into_inner())
.clone();
if let Some(hook) = hook_slot {
return hook(bootstrap, env_vars, operation);
}
}
if !root_privilege_drop_supported() {
return Err(unsupported_root_privilege_drop_error());
}
match bootstrap.execution_mode {
ExecutionMode::InProcess => Err(BootstrapError::from(eyre!(concat!(
"ExecutionMode::InProcess is unsafe for root because process-wide ",
"UID/GID changes race in multi-threaded tests; switch to ",
"ExecutionMode::Subprocess"
)))),
ExecutionMode::Subprocess => spawn_worker_inner(bootstrap, env_vars, operation),
}
}
#[cfg(all(unix, privileged_unix_platform))]
fn spawn_worker_inner(
bootstrap: &TestBootstrapSettings,
env_vars: &[(String, Option<String>)],
operation: WorkerOperation,
) -> BootstrapResult<()> {
let worker = bootstrap.worker_binary.as_ref().ok_or_else(|| {
BootstrapError::from(eyre!(concat!(
"pg_worker binary not found. Install it with 'cargo install --path . --bin pg_worker' ",
"and ensure it is in PATH, or set PG_EMBEDDED_WORKER to its absolute path"
)))
})?;
let args = WorkerRequestArgs {
worker,
settings: &bootstrap.settings,
env_vars,
operation,
timeout: operation.timeout(bootstrap),
};
let request = WorkerRequest::new(args);
worker_process::run(&request)
}
#[cfg(not(all(
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
),
)))]
fn spawn_worker_inner(
_bootstrap: &TestBootstrapSettings,
_env_vars: &[(String, Option<String>)],
operation: WorkerOperation,
) -> BootstrapResult<()> {
Err(BootstrapError::from(eyre!(
"privilege drop not supported on this target; refusing to run as root: {}",
operation.error_context()
)))
}
fn log_failure(operation: WorkerOperation, err: &BootstrapError) {
error!(
target: LOG_TARGET,
operation = operation.as_str(),
error = %err,
"lifecycle operation failed"
);
}
fn log_success(operation: WorkerOperation) {
info!(
target: LOG_TARGET,
operation = operation.as_str(),
"lifecycle operation completed"
);
}
fn log_in_process_start(operation: WorkerOperation, async_mode: bool) {
let suffix = if async_mode { " (async)" } else { "" };
info!(
target: LOG_TARGET,
operation = operation.as_str(),
"running lifecycle operation in-process{suffix}"
);
}
fn log_worker_dispatch(operation: WorkerOperation, worker_binary: Option<&str>, async_mode: bool) {
let suffix = if async_mode { " (async)" } else { "" };
info!(
target: LOG_TARGET,
operation = operation.as_str(),
worker = worker_binary,
"dispatching lifecycle operation via worker{suffix}"
);
}
fn timeout_error(ctx: &'static str, timeout: std::time::Duration) -> BootstrapError {
BootstrapError::from(eyre!(
"{ctx}: operation timed out after {:.1}s",
timeout.as_secs_f64()
))
}
async fn run_with_timeout<Fut>(
timeout: std::time::Duration,
future: Fut,
) -> Result<Result<(), postgresql_embedded::Error>, tokio::time::error::Elapsed>
where
Fut: Future<Output = Result<(), postgresql_embedded::Error>> + Send,
{
tokio::time::timeout(timeout, future).await
}
#[derive(Debug)]
#[doc(hidden)]
pub struct WorkerInvoker<'a> {
runtime: &'a Runtime,
bootstrap: &'a TestBootstrapSettings,
env_vars: &'a [(String, Option<String>)],
}
impl<'a> WorkerInvoker<'a> {
pub const fn new(
runtime: &'a Runtime,
bootstrap: &'a TestBootstrapSettings,
env_vars: &'a [(String, Option<String>)],
) -> Self {
Self {
runtime,
bootstrap,
env_vars,
}
}
pub fn invoke<Fut>(&self, operation: WorkerOperation, in_process_op: Fut) -> BootstrapResult<()>
where
Fut: Future<Output = Result<(), postgresql_embedded::Error>> + Send,
{
let span = self.lifecycle_span(operation);
let _entered = span.enter();
let result = self.dispatch_operation(operation, in_process_op);
Self::log_outcome(operation, &result);
result
}
fn dispatch_operation<Fut>(
&self,
operation: WorkerOperation,
in_process_op: Fut,
) -> BootstrapResult<()>
where
Fut: Future<Output = Result<(), postgresql_embedded::Error>> + Send,
{
match self.bootstrap.privileges {
ExecutionPrivileges::Unprivileged => self.run_unprivileged(operation, in_process_op),
ExecutionPrivileges::Root => self.run_root(operation),
}
}
fn run_unprivileged<Fut>(
&self,
operation: WorkerOperation,
in_process_op: Fut,
) -> BootstrapResult<()>
where
Fut: Future<Output = Result<(), postgresql_embedded::Error>> + Send,
{
log_in_process_start(operation, false);
let timeout = operation.timeout(self.bootstrap);
self.invoke_unprivileged(in_process_op, operation.error_context(), timeout)
}
fn run_root(&self, operation: WorkerOperation) -> BootstrapResult<()> {
log_worker_dispatch(
operation,
self.bootstrap.worker_binary.as_ref().map(|p| p.as_str()),
false,
);
self.invoke_as_root(operation)
}
fn log_outcome(operation: WorkerOperation, result: &BootstrapResult<()>) {
if let Err(err) = result {
log_failure(operation, err);
} else {
log_success(operation);
}
}
fn invoke_unprivileged<Fut>(
&self,
future: Fut,
ctx: &'static str,
timeout: std::time::Duration,
) -> BootstrapResult<()>
where
Fut: Future<Output = Result<(), postgresql_embedded::Error>> + Send,
{
let result = if tokio::runtime::Handle::try_current().is_ok() {
self.run_unprivileged_in_scoped_thread(future, ctx, timeout)?
} else {
self.runtime.block_on(run_with_timeout(timeout, future))
};
result
.map_err(|_| timeout_error(ctx, timeout))?
.context(ctx)
.map_err(BootstrapError::from)
}
fn run_unprivileged_in_scoped_thread<Fut>(
&self,
future: Fut,
ctx: &'static str,
timeout: std::time::Duration,
) -> BootstrapResult<Result<Result<(), postgresql_embedded::Error>, tokio::time::error::Elapsed>>
where
Fut: Future<Output = Result<(), postgresql_embedded::Error>> + Send,
{
let runtime = self.runtime;
std::thread::scope(|scope| {
scope
.spawn(move || runtime.block_on(run_with_timeout(timeout, future)))
.join()
})
.map_err(|panic_payload| {
nested_runtime_thread_panic(ctx, "nested-runtime operation", panic_payload)
})
}
fn lifecycle_span(&self, operation: WorkerOperation) -> tracing::Span {
create_lifecycle_span(operation, self.bootstrap, false)
}
pub(super) fn invoke_as_root(&self, operation: WorkerOperation) -> BootstrapResult<()> {
execute_root_operation(self.bootstrap, self.env_vars, operation)
}
}
#[cfg(feature = "async-api")]
mod async_invoker;
#[cfg(feature = "async-api")]
pub(crate) use async_invoker::AsyncInvoker;
#[cfg(test)]
mod tests;