use std::path::Path;
use std::sync::Mutex;
use std::time::Duration;
use crate::CleanupMode;
use crate::error::BootstrapResult;
use postgresql_embedded::Settings;
struct ShutdownState {
settings: Settings,
shutdown_timeout: Duration,
cleanup_mode: CleanupMode,
}
static SHUTDOWN_STATE: Mutex<Option<ShutdownState>> = Mutex::new(None);
const POLL_INTERVAL: Duration = Duration::from_millis(50);
const POST_SIGKILL_GRACE: Duration = Duration::from_millis(100);
pub(super) fn register_shutdown_hook(
settings: Settings,
shutdown_timeout: Duration,
cleanup_mode: CleanupMode,
) -> BootstrapResult<()> {
let mut guard = SHUTDOWN_STATE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if guard.is_some() {
log_already_registered();
return Ok(());
}
register_atexit()?;
*guard = Some(ShutdownState {
settings,
shutdown_timeout,
cleanup_mode,
});
log_registration_success();
Ok(())
}
fn log_already_registered() {
tracing::debug!(
target: crate::observability::LOG_TARGET,
"shutdown hook already registered; skipping duplicate registration"
);
}
fn log_registration_success() {
tracing::debug!(
target: crate::observability::LOG_TARGET,
"registered atexit shutdown hook for PostgreSQL postmaster"
);
}
fn register_atexit() -> BootstrapResult<()> {
let rc = unsafe { libc::atexit(shutdown_callback) };
if rc != 0 {
return Err(color_eyre::eyre::eyre!("libc::atexit registration failed (rc={rc})").into());
}
Ok(())
}
extern "C" fn shutdown_callback() {
let Ok(guard) = SHUTDOWN_STATE.try_lock() else {
return;
};
let Some(state) = guard.as_ref() else {
return;
};
let Some(pid) = read_postmaster_pid(&state.settings.data_dir) else {
best_effort_cleanup(state);
return;
};
if !process_is_running(pid) {
best_effort_cleanup(state);
return;
}
stop_postmaster(pid, state);
best_effort_cleanup(state);
}
fn stop_postmaster(pid: libc::pid_t, state: &ShutdownState) {
send_sigterm(pid);
if wait_for_exit(pid, state.shutdown_timeout) {
return;
}
send_sigkill(pid);
std::thread::sleep(POST_SIGKILL_GRACE);
}
fn send_sigterm(pid: libc::pid_t) {
unsafe {
libc::kill(pid, libc::SIGTERM);
}
}
fn send_sigkill(pid: libc::pid_t) {
unsafe {
libc::kill(pid, libc::SIGKILL);
}
}
fn wait_for_exit(pid: libc::pid_t, timeout: Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
loop {
if !process_is_running(pid) {
return true;
}
if std::time::Instant::now() >= deadline {
return false;
}
std::thread::sleep(POLL_INTERVAL);
}
}
#[must_use]
pub fn read_postmaster_pid(data_dir: &Path) -> Option<libc::pid_t> {
let pid_file = data_dir.join("postmaster.pid");
let contents = std::fs::read_to_string(&pid_file).ok()?;
let first_line = contents.lines().next()?;
let pid = first_line.trim().parse::<libc::pid_t>().ok()?;
if pid > 0 { Some(pid) } else { None }
}
#[must_use]
pub fn process_is_running(pid: libc::pid_t) -> bool {
if pid <= 0 {
return false;
}
let rc = unsafe { libc::kill(pid, 0) };
if rc == 0 {
return true;
}
!matches!(
std::io::Error::last_os_error().raw_os_error(),
Some(code) if code == libc::ESRCH
)
}
fn best_effort_cleanup(state: &ShutdownState) {
super::cleanup::cleanup_in_process(state.cleanup_mode, &state.settings, "atexit-shutdown-hook");
}
#[cfg(all(test, feature = "cluster-unit-tests"))]
mod tests {
use super::*;
use color_eyre::eyre::{Result, ensure};
use rstest::{fixture, rstest};
use tempfile::TempDir;
#[fixture]
fn pid_dir() -> Result<TempDir> {
Ok(tempfile::tempdir()?)
}
#[rstest]
#[case::valid_file(Some("12345\nother\nlines\n"), Some(12345))]
#[case::missing_file(None, None)]
#[case::empty_file(Some(""), None)]
#[case::zero_pid(Some("0\n"), None)]
#[case::negative_pid(Some("-1\n"), None)]
fn read_postmaster_pid_parses_first_line(
pid_dir: Result<TempDir>,
#[case] file_content: Option<&str>,
#[case] expected: Option<libc::pid_t>,
) -> Result<()> {
let dir = pid_dir?;
if let Some(content) = file_content {
std::fs::write(dir.path().join("postmaster.pid"), content)?;
}
let result = read_postmaster_pid(dir.path());
ensure!(result == expected, "expected {expected:?}, got {result:?}");
Ok(())
}
#[test]
fn process_is_running_returns_true_for_current_process() -> Result<()> {
let pid = libc::pid_t::try_from(std::process::id())?;
ensure!(process_is_running(pid), "current process should be running");
Ok(())
}
#[test]
fn process_is_running_returns_false_for_nonexistent_pid() -> Result<()> {
ensure!(
!process_is_running(i32::MAX),
"nonexistent PID should not be running"
);
Ok(())
}
#[rstest]
#[case::zero(0)]
#[case::negative(-1)]
fn process_is_running_rejects_non_positive_pid(#[case] pid: libc::pid_t) -> Result<()> {
ensure!(
!process_is_running(pid),
"non-positive PID {pid} should not be considered running"
);
Ok(())
}
}