use std::sync::{Arc, Condvar, Mutex};
use query_planning::error::{QueryError, QueryErrorCode, Result};
use crate::{
observability::RuntimeObservationHandle,
runtime::{
PoolBlockingWork,
context::ExecutionCancellation,
gpu_execution_pool::{GpuExecutionJobKind, GpuExecutorLease},
process_exit::{
ProcessExitPhase, ProcessExitRegistration, ProcessExitShutdown, register_cuda_process_exit_shutdown,
},
},
};
pub enum SupervisedWorkerJoin<T> {
Completed(T),
Panicked,
Cancelled,
}
pub struct SupervisedWorker<T> {
supervisor: Arc<SupervisedWorkerSupervisor<T>>,
}
impl<T: Send + 'static> SupervisedWorker<T> {
#[doc(hidden)]
pub fn submit<F>(
cancellation: ExecutionCancellation,
executor: &GpuExecutorLease,
observation: RuntimeObservationHandle,
worker: F,
) -> Result<Self>
where
F: FnOnce() -> T + Send + 'static,
{
let supervisor = SupervisedWorkerSupervisor::new(cancellation);
supervisor.activate_process_exit();
supervisor.spawn_on(executor, observation, worker)
}
#[doc(hidden)]
pub fn join(self) -> Option<SupervisedWorkerJoin<T>> {
self.supervisor.join()
}
}
struct SupervisedWorkerSupervisor<T> {
cancellation: ExecutionCancellation,
state: Mutex<SupervisedWorkerSupervisorState<T>>,
state_changed: Condvar,
process_exit_registration: Mutex<Option<ProcessExitRegistration>>,
}
struct SupervisedWorkerSupervisorState<T> {
process_exit_started: bool,
worker: Option<PoolBlockingWork<T>>,
joining: bool,
joined: bool,
}
impl<T> SupervisedWorkerSupervisor<T> {
fn new(cancellation: ExecutionCancellation) -> Arc<Self> {
Arc::new(Self {
cancellation,
state: Mutex::new(SupervisedWorkerSupervisorState {
process_exit_started: false,
worker: None,
joining: false,
joined: false,
}),
state_changed: Condvar::new(),
process_exit_registration: Mutex::new(None),
})
}
fn request_process_exit(&self) {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
state.process_exit_started = true;
drop(state);
self.cancellation.cancel();
}
fn unregister_process_exit(&self) {
let registration =
self.process_exit_registration.lock().unwrap_or_else(std::sync::PoisonError::into_inner).take();
if let Some(registration) = registration {
registration.unregister();
}
}
fn finish_join(&self) {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
state.joining = false;
state.joined = true;
self.state_changed.notify_all();
drop(state);
self.unregister_process_exit();
}
fn take_or_wait_for_join(&self) -> Option<PoolBlockingWork<T>> {
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
loop {
if let Some(worker) = state.worker.take() {
state.joining = true;
return Some(worker);
}
if state.joining {
state = self.state_changed.wait(state).unwrap_or_else(std::sync::PoisonError::into_inner);
continue;
}
return None;
}
}
fn join(&self) -> Option<SupervisedWorkerJoin<T>> {
let worker = self.take_or_wait_for_join()?;
let outcome = match worker.join() {
Ok(value) => SupervisedWorkerJoin::Completed(value),
Err(error) if error.code() == QueryErrorCode::Internal => SupervisedWorkerJoin::Panicked,
Err(_) => SupervisedWorkerJoin::Cancelled,
};
self.finish_join();
Some(outcome)
}
#[cfg(test)]
fn joined_for_test(&self) -> bool {
self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner).joined
}
}
impl<T: Send + 'static> SupervisedWorkerSupervisor<T> {
fn activate_process_exit(self: &Arc<Self>) {
let shutdown: Arc<dyn ProcessExitShutdown> = Arc::clone(self) as _;
let registration = register_cuda_process_exit_shutdown(shutdown);
*self.process_exit_registration.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = registration;
}
fn spawn_on<F>(
self: &Arc<Self>,
executor: &GpuExecutorLease,
observation: RuntimeObservationHandle,
worker: F,
) -> Result<SupervisedWorker<T>>
where
F: FnOnce() -> T + Send + 'static,
{
let mut state = self.state.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
if state.process_exit_started {
drop(state);
self.unregister_process_exit();
return Err(QueryError::new(
QueryErrorCode::Cancelled,
"query_engine.runtime.supervised_worker.submit",
"supervised blocking worker cannot start after process exit begins",
)
.with_fact("reason", "process_exit_started"));
}
let work = match executor.submit_blocking(GpuExecutionJobKind::AttemptWork, observation, worker) {
Ok(work) => work,
Err(error) => {
drop(state);
self.unregister_process_exit();
return Err(error);
},
};
state.worker = Some(work);
Ok(SupervisedWorker { supervisor: Arc::clone(self) })
}
}
impl<T: Send + 'static> ProcessExitShutdown for SupervisedWorkerSupervisor<T> {
fn phase(&self) -> ProcessExitPhase {
ProcessExitPhase::SupervisedWorker
}
fn shutdown_for_process_exit(&self) {
self.request_process_exit();
let _ = self.join();
self.unregister_process_exit();
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
use crate::runtime::gpu_execution_pool::GpuExecutionPool;
fn test_executor() -> (GpuExecutionPool, GpuExecutorLease) {
let pool = GpuExecutionPool::new(0, 2).expect("test device executor");
let executor = GpuExecutorLease::from_pool(pool.clone());
(pool, executor)
}
#[test]
fn process_exit_joins_supervised_worker_and_prevents_late_spawn() {
let (_pool, executor) = test_executor();
let cancellation = ExecutionCancellation::new();
let supervisor = SupervisedWorkerSupervisor::new(cancellation.clone());
let (started_sender, started_receiver) = std::sync::mpsc::sync_channel(0);
let worker_cancellation = cancellation.clone();
let worker = supervisor
.spawn_on(&executor, RuntimeObservationHandle::disabled(), move || {
started_sender.send(()).unwrap();
while !worker_cancellation.is_cancelled() {
std::thread::yield_now();
}
7_u8
})
.unwrap();
started_receiver.recv_timeout(Duration::from_secs(5)).unwrap();
supervisor.shutdown_for_process_exit();
assert!(cancellation.is_cancelled());
assert!(supervisor.joined_for_test());
assert!(worker.join().is_none(), "process-exit owner already joined the worker");
let late_spawn = supervisor.spawn_on(&executor, RuntimeObservationHandle::disabled(), || 9_u8);
let error = match late_spawn {
Ok(_) => panic!("supervised worker unexpectedly spawned after process exit"),
Err(error) => error,
};
assert_eq!(error.code(), QueryErrorCode::Cancelled);
assert_eq!(error.fact_value("reason"), Some("process_exit_started"));
}
#[test]
fn submit_returns_independent_workers_that_each_join_once() {
let pool = GpuExecutionPool::new(0, 3).expect("test device executor");
let executor = GpuExecutorLease::from_pool(pool);
let first = SupervisedWorker::submit(
ExecutionCancellation::new(),
&executor,
RuntimeObservationHandle::disabled(),
|| 7_u8,
)
.expect("first submit");
let second = SupervisedWorker::submit(
ExecutionCancellation::new(),
&executor,
RuntimeObservationHandle::disabled(),
|| 9_u8,
)
.expect("second submit");
assert!(matches!(first.join(), Some(SupervisedWorkerJoin::Completed(7_u8))));
assert!(matches!(second.join(), Some(SupervisedWorkerJoin::Completed(9_u8))));
}
#[test]
fn submit_propagates_worker_panic_through_join() {
let (_pool, executor) = test_executor();
let worker = SupervisedWorker::submit(
ExecutionCancellation::new(),
&executor,
RuntimeObservationHandle::disabled(),
|| -> u8 { panic!("supervised worker panic probe") },
)
.expect("submit");
assert!(matches!(worker.join(), Some(SupervisedWorkerJoin::Panicked)));
}
}