use std::{
collections::BTreeMap,
ffi::c_int,
panic::{AssertUnwindSafe, catch_unwind},
sync::{Arc, Mutex, OnceLock},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum ProcessExitPhase {
BackendCoordinator,
SupervisedWorker,
ExecutionPool,
StreamPool,
ObservationCompletion,
}
pub(crate) trait ProcessExitShutdown: Send + Sync {
fn phase(&self) -> ProcessExitPhase;
fn shutdown_for_process_exit(&self);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ProcessExitRegistration {
id: Option<u64>,
}
impl ProcessExitRegistration {
pub(crate) fn unregister(self) {
if let Some(id) = self.id {
global_registry().unregister(id);
}
}
}
#[derive(Default)]
struct ProcessExitRegistry {
state: Mutex<ProcessExitRegistryState>,
}
#[derive(Default)]
struct ProcessExitRegistryState {
next_id: u64,
registrations: BTreeMap<u64, Arc<dyn ProcessExitShutdown>>,
shutdown_started: bool,
}
impl ProcessExitRegistry {
fn register(&self, shutdown: Arc<dyn ProcessExitShutdown>) -> ProcessExitRegistration {
let id = {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if state.shutdown_started {
None
} else {
let id = state.next_id;
state.next_id = state.next_id.checked_add(1).expect("process-exit worker registration id overflow");
state.registrations.insert(id, Arc::clone(&shutdown));
Some(id)
}
};
if id.is_none() {
shutdown_owner(&shutdown);
}
ProcessExitRegistration { id }
}
fn unregister(&self, id: u64) {
self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner).registrations.remove(&id);
}
fn shutdown(&self) {
let mut registrations = {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if state.shutdown_started {
return;
}
state.shutdown_started = true;
std::mem::take(&mut state.registrations).into_values().collect::<Vec<_>>()
};
registrations.sort_by_key(|shutdown| shutdown.phase());
for shutdown in registrations {
shutdown_owner(&shutdown);
}
}
#[cfg(test)]
fn registration_count(&self) -> usize {
self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner).registrations.len()
}
}
fn shutdown_owner(shutdown: &Arc<dyn ProcessExitShutdown>) {
let _ = catch_unwind(AssertUnwindSafe(|| shutdown.shutdown_for_process_exit()));
}
static GLOBAL_PROCESS_EXIT_REGISTRY: OnceLock<ProcessExitRegistry> = OnceLock::new();
fn global_registry() -> &'static ProcessExitRegistry {
GLOBAL_PROCESS_EXIT_REGISTRY.get_or_init(|| {
let status = unsafe { atexit(process_exit_shutdown) };
assert_eq!(status, 0, "failed to register query-engine process-exit shutdown hook");
ProcessExitRegistry::default()
})
}
pub(crate) fn register_cuda_process_exit_shutdown(
shutdown: Arc<dyn ProcessExitShutdown>,
) -> Option<ProcessExitRegistration> {
cudf::DeviceId::current().ok()?;
Some(global_registry().register(shutdown))
}
extern "C" fn process_exit_shutdown() {
if let Some(registry) = GLOBAL_PROCESS_EXIT_REGISTRY.get() {
let _ = catch_unwind(AssertUnwindSafe(|| registry.shutdown()));
}
}
unsafe extern "C" {
fn atexit(callback: extern "C" fn()) -> c_int;
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
#[derive(Debug)]
struct ShutdownProbe {
phase: ProcessExitPhase,
label: &'static str,
events: Arc<Mutex<Vec<&'static str>>>,
}
impl ProcessExitShutdown for ShutdownProbe {
fn phase(&self) -> ProcessExitPhase {
self.phase
}
fn shutdown_for_process_exit(&self) {
self.events.lock().unwrap().push(self.label);
}
}
#[test]
fn registry_orders_dependencies_and_drops_finished_workers() {
let registry = ProcessExitRegistry::default();
let events = Arc::new(Mutex::new(Vec::new()));
let stream = registry.register(Arc::new(ShutdownProbe {
phase: ProcessExitPhase::StreamPool,
label: "stream",
events: Arc::clone(&events),
}));
registry.register(Arc::new(ShutdownProbe {
phase: ProcessExitPhase::BackendCoordinator,
label: "backend",
events: Arc::clone(&events),
}));
registry.register(Arc::new(ShutdownProbe {
phase: ProcessExitPhase::ExecutionPool,
label: "execution",
events: Arc::clone(&events),
}));
registry.register(Arc::new(ShutdownProbe {
phase: ProcessExitPhase::SupervisedWorker,
label: "supervised-worker",
events: Arc::clone(&events),
}));
registry.unregister(stream.id.expect("local registration has an id"));
assert_eq!(registry.registration_count(), 3);
registry.shutdown();
assert_eq!(*events.lock().unwrap(), ["backend", "supervised-worker", "execution"]);
}
#[test]
fn late_registration_shuts_down_inline_after_the_registry_snapshot() {
let registry = ProcessExitRegistry::default();
let events = Arc::new(Mutex::new(Vec::new()));
registry.register(Arc::new(ShutdownProbe {
phase: ProcessExitPhase::BackendCoordinator,
label: "snapshot",
events: Arc::clone(&events),
}));
registry.shutdown();
registry.shutdown();
let late = registry.register(Arc::new(ShutdownProbe {
phase: ProcessExitPhase::ExecutionPool,
label: "late",
events: Arc::clone(&events),
}));
assert_eq!(*events.lock().unwrap(), ["snapshot", "late"]);
assert_eq!(registry.registration_count(), 0);
late.unregister();
}
}