use std::collections::HashSet;
use std::sync::{Arc, mpsc};
use std::time::{Duration, Instant};
use beamr::scheduler::EXIT_EVENT_CAPACITY;
use super::{ProcessEnding, ProcessExitRegistry};
use crate::EngineError;
use crate::runtime::{RuntimeConfig, RuntimeHandle, SignalDeliveryConfig, UndeliveredWake};
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[test]
fn runtime_claims_the_only_exit_event_subscription_with_typed_duplicate_failure() -> TestResult {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(
Some(1),
crate::runtime::config::TEST_STOP_DRAIN_TIMEOUT,
))?);
assert!(runtime.scheduler.subscribe_exit_events().is_none());
let duplicate = ProcessExitRegistry::new(
Arc::clone(&runtime.scheduler),
runtime.stop_drain_timeout(),
runtime.signal_delivery().max_enqueue_attempts as usize,
)
.err()
.ok_or("a second process-exit registry claimed the scheduler subscription")?;
assert!(matches!(
duplicate,
EngineError::ProcessExitSubscriptionUnavailable
));
runtime.shutdown()?;
Ok(())
}
#[test]
fn lagged_stream_resynchronizes_every_registered_outcome() -> TestResult {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(
Some(1),
crate::runtime::config::TEST_STOP_DRAIN_TIMEOUT,
))?);
runtime.process_exits.pause_for_test();
let first_pid = runtime.spawn_test_process()?;
runtime.cancel_pid(first_pid)?;
runtime
.process_exits
.wait_for_pause_for_test(Duration::from_secs(10))?;
let mut pids = Vec::with_capacity(EXIT_EVENT_CAPACITY + 2);
pids.push(first_pid);
for _ in 0..=EXIT_EVENT_CAPACITY {
let pid = runtime.spawn_test_process()?;
runtime.cancel_pid(pid)?;
pids.push(pid);
}
let (sender, receiver) = mpsc::channel();
for pid in &pids {
let callback_sender = sender.clone();
let callback_pid = *pid;
runtime.monitor_process_for_test(*pid, move |outcome| {
let _ = callback_sender.send((callback_pid, outcome.is_ok()));
})?;
}
drop(sender);
runtime.process_exits.release_for_test();
let deadline = Instant::now() + Duration::from_secs(20);
let mut observed = HashSet::with_capacity(pids.len());
while observed.len() < pids.len() {
let remaining = deadline.saturating_duration_since(Instant::now());
let (pid, outcome_available) = receiver.recv_timeout(remaining)?;
if !outcome_available {
return Err(format!("process {pid} did not receive its cached exit outcome").into());
}
observed.insert(pid);
}
wait_until(deadline, || {
runtime.process_exits.lag_recoveries_for_test() > 0
&& pids
.iter()
.all(|pid| runtime.process_cleanup_complete_for_test(*pid))
})?;
assert_eq!(observed.len(), pids.len());
assert!(runtime.process_exits.lag_recoveries_for_test() > 0);
runtime.shutdown()?;
Ok(())
}
fn wait_until(deadline: Instant, predicate: impl Fn() -> bool) -> TestResult {
while Instant::now() < deadline {
if predicate() {
return Ok(());
}
std::thread::sleep(Duration::from_millis(1));
}
Err("process-exit condition did not become true before its deadline".into())
}
#[test]
fn a_never_registered_pid_below_the_watermark_has_no_recorded_ending() -> TestResult {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(
Some(1),
crate::runtime::config::TEST_STOP_DRAIN_TIMEOUT,
))?);
let registered = runtime.spawn_test_process()?;
let above = registered
.checked_add(1)
.ok_or("fixture control: the registration watermark is at the pid ceiling")?;
let never_allocated = 0;
let mut unregistered = vec![never_allocated];
unregistered.extend(
(1..registered)
.rev()
.filter(|pid| !runtime.process_exits.contains(*pid))
.take(4),
);
for pid in unregistered {
assert!(
runtime.process_exits.below_registration_watermark(pid),
"fixture control: pid {pid} must sit at or below the registration watermark \
with no record held"
);
assert_eq!(
runtime.process_exits.ending_knowledge(pid)?,
ProcessEnding::Forgotten,
"an unregistered pid below the watermark is forgotten, not terminal"
);
assert!(
!runtime.process_exits.has_terminal(pid)?,
"pid {pid} was never registered here, so the registry holds no ending for it"
);
assert!(
!runtime.process_ending_recorded(pid)?,
"the shared completion classifier must never read the watermark as an ending"
);
}
assert_eq!(
runtime.process_exits.ending_knowledge(registered)?,
ProcessEnding::Pending
);
assert!(!runtime.process_exits.has_terminal(registered)?);
assert_eq!(
runtime.process_exits.ending_knowledge(above)?,
ProcessEnding::NeverRegistered
);
assert!(!runtime.process_exits.has_terminal(above)?);
runtime.shutdown()?;
Ok(())
}
#[test]
fn a_published_terminal_on_a_held_record_is_a_recorded_ending() -> TestResult {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(
Some(1),
crate::runtime::config::TEST_STOP_DRAIN_TIMEOUT,
))?);
let pid = runtime.spawn_test_process()?;
runtime.cancel_pid(pid)?;
let record = runtime
.process_exits
.find(pid)
.ok_or("fixture control: a spawned process must hold an exit record")?;
drop(record.wait()?);
assert_eq!(
runtime.process_exits.ending_knowledge(pid)?,
ProcessEnding::Recorded
);
assert!(runtime.process_exits.has_terminal(pid)?);
assert!(
runtime.process_ending_recorded(pid)?,
"a published terminal is a recorded ending without any cleanup tombstone"
);
runtime.shutdown()?;
Ok(())
}
const EXIT_IN_FLIGHT_FIXTURE_TIMEOUT: Duration = Duration::from_secs(10);
fn runtime_with_ready_timeout(
ready_timeout: Duration,
) -> Result<Arc<RuntimeHandle>, Box<dyn std::error::Error>> {
let delivery = SignalDeliveryConfig::new(
ready_timeout,
1,
Duration::from_millis(1),
Duration::from_millis(1),
);
Ok(Arc::new(RuntimeHandle::new(
RuntimeConfig::new(Some(1), crate::runtime::config::TEST_STOP_DRAIN_TIMEOUT)
.with_signal_delivery(delivery),
)?))
}
fn exit_in_flight_pid(runtime: &Arc<RuntimeHandle>) -> Result<u64, Box<dyn std::error::Error>> {
let pid = runtime.spawn_test_process()?;
runtime.cancel_pid(pid)?;
wait_until(Instant::now() + EXIT_IN_FLIGHT_FIXTURE_TIMEOUT, || {
!runtime.is_live(pid)
})?;
assert!(
!runtime.process_cleanup_started(pid),
"fixture control: Aion cleanup cannot have started without a monitor"
);
assert_eq!(
runtime.process_exits.ending_knowledge(pid)?,
ProcessEnding::Pending,
"fixture control: the held drainer must leave the record unpublished"
);
Ok(pid)
}
#[test]
fn an_exit_in_flight_is_waited_out_and_resolves_to_the_recorded_ending() -> TestResult {
let runtime = runtime_with_ready_timeout(EXIT_IN_FLIGHT_FIXTURE_TIMEOUT)?;
runtime.pause_exit_drainer_for_test(EXIT_IN_FLIGHT_FIXTURE_TIMEOUT)?;
let pid = exit_in_flight_pid(&runtime)?;
let releaser = {
let runtime = Arc::clone(&runtime);
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(5));
runtime.release_exit_drainer_for_test();
})
};
let verdict = runtime.classify_undelivered_wake(pid);
releaser
.join()
.map_err(|_| "the drainer-release thread panicked")?;
assert_eq!(
verdict?,
UndeliveredWake::ProcessEnded,
"an exit that publishes inside the readiness window is a recorded ending"
);
runtime.shutdown()?;
Ok(())
}
#[test]
fn an_exit_publishing_nothing_inside_the_window_is_reported_still_in_flight() -> TestResult {
let runtime = runtime_with_ready_timeout(Duration::from_millis(20))?;
runtime.pause_exit_drainer_for_test(EXIT_IN_FLIGHT_FIXTURE_TIMEOUT)?;
let pid = exit_in_flight_pid(&runtime)?;
let verdict = runtime.classify_undelivered_wake(pid);
runtime.release_exit_drainer_for_test();
assert_eq!(
verdict?,
UndeliveredWake::ExitInFlight,
"a terminal that never publishes inside the window leaves the exit in flight"
);
wait_until(Instant::now() + EXIT_IN_FLIGHT_FIXTURE_TIMEOUT, || {
matches!(
runtime.classify_undelivered_wake(pid),
Ok(UndeliveredWake::ProcessEnded)
)
})?;
runtime.shutdown()?;
Ok(())
}
#[test]
fn a_terminal_published_before_the_registry_read_is_an_ending_not_a_refusal() -> TestResult {
let runtime = runtime_with_ready_timeout(Duration::from_millis(20))?;
let pid = runtime.spawn_test_process()?;
runtime.cancel_pid(pid)?;
wait_until(Instant::now() + EXIT_IN_FLIGHT_FIXTURE_TIMEOUT, || {
matches!(runtime.process_exits.has_terminal(pid), Ok(true))
})?;
wait_until(Instant::now() + EXIT_IN_FLIGHT_FIXTURE_TIMEOUT, || {
!runtime.is_live(pid)
})?;
assert!(
!runtime.process_cleanup_started(pid),
"fixture control: no monitor is installed, so no tombstone can exist"
);
assert_eq!(
runtime.process_exits.ending_knowledge(pid)?,
ProcessEnding::Recorded,
"fixture control: the registry must still hold the published terminal"
);
assert_eq!(
runtime.classify_undelivered_wake(pid)?,
UndeliveredWake::ProcessEnded,
"a terminal the registry holds is an ending on the read that finds it"
);
runtime.shutdown()?;
Ok(())
}