use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CleanupExpectation {
Strong,
BestEffort { sweep_budget_ms: u64 },
}
impl CleanupExpectation {
pub fn for_current_tier(tier_label: Option<&str>) -> Self {
#[cfg(target_os = "windows")]
{
let _ = tier_label;
CleanupExpectation::Strong
}
#[cfg(target_os = "linux")]
{
match tier_label {
Some("full") => CleanupExpectation::Strong,
_ => CleanupExpectation::BestEffort {
sweep_budget_ms: crate::proctree::MAX_EXTINCTION_DEADLINE_MS,
},
}
}
#[cfg(target_os = "macos")]
{
let _ = tier_label;
CleanupExpectation::BestEffort {
sweep_budget_ms: crate::proctree::MAX_EXTINCTION_DEADLINE_MS,
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = tier_label;
CleanupExpectation::BestEffort {
sweep_budget_ms: crate::proctree::MAX_EXTINCTION_DEADLINE_MS,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KillOutcome {
Exited,
KilledOnDeadline,
}
pub trait WaitKill {
fn try_wait(&mut self) -> Option<i32>;
fn terminate(&mut self);
fn terminate_graceful(&mut self);
}
impl WaitKill for crate::sandbox::SandboxHandle {
fn try_wait(&mut self) -> Option<i32> {
crate::sandbox::SandboxHandle::try_wait(self)
}
fn terminate(&mut self) {
crate::sandbox::SandboxHandle::terminate(self);
}
fn terminate_graceful(&mut self) {
crate::sandbox::SandboxHandle::terminate_graceful(self);
}
}
pub fn kill_on_deadline(
handle: &mut crate::sandbox::SandboxHandle,
deadline: Instant,
poll: Duration,
) -> (KillOutcome, i32) {
kill_on_deadline_with(handle, deadline, poll)
}
pub fn kill_on_deadline_with<H: WaitKill>(
handle: &mut H,
deadline: Instant,
poll: Duration,
) -> (KillOutcome, i32) {
kill_on_deadline_with_grace(handle, deadline, poll, Duration::from_secs(2))
}
pub fn kill_on_deadline_with_grace<H: WaitKill>(
handle: &mut H,
deadline: Instant,
poll: Duration,
grace: Duration,
) -> (KillOutcome, i32) {
loop {
if let Some(code) = handle.try_wait() {
return (KillOutcome::Exited, code);
}
if Instant::now() >= deadline {
handle.terminate_graceful();
let grace_end = Instant::now() + grace;
loop {
if let Some(code) = handle.try_wait() {
return (KillOutcome::KilledOnDeadline, code);
}
if Instant::now() >= grace_end {
break;
}
std::thread::sleep(poll);
}
handle.terminate();
let end = Instant::now() + Duration::from_secs(10);
loop {
if let Some(code) = handle.try_wait() {
return (KillOutcome::KilledOnDeadline, code);
}
if Instant::now() >= end {
handle.terminate();
return (KillOutcome::KilledOnDeadline, -1);
}
std::thread::sleep(poll);
}
}
std::thread::sleep(poll);
}
}
#[cfg(test)]
mod killer_tests {
use super::*;
use std::collections::VecDeque;
struct FakeHandle {
polls: VecDeque<Option<i32>>,
terminates: usize,
graceful_terminates: usize,
exit_on_graceful: bool,
}
impl WaitKill for FakeHandle {
fn try_wait(&mut self) -> Option<i32> {
self.polls.pop_front().flatten()
}
fn terminate(&mut self) {
self.terminates += 1;
self.polls.push_front(Some(-9));
}
fn terminate_graceful(&mut self) {
self.graceful_terminates += 1;
if self.exit_on_graceful {
self.polls.push_front(Some(-15));
}
}
}
#[test]
fn exits_before_deadline_without_terminate() {
let mut h = FakeHandle {
polls: vec![None, None, Some(0)].into(),
terminates: 0,
graceful_terminates: 0,
exit_on_graceful: false,
};
let (outcome, code) = kill_on_deadline_with(
&mut h,
Instant::now() + Duration::from_secs(30),
Duration::from_millis(1),
);
assert_eq!((outcome, code), (KillOutcome::Exited, 0));
assert_eq!(h.terminates, 0);
assert_eq!(h.graceful_terminates, 0);
}
#[test]
fn deadline_graceful_exit_reaps_without_sigkill() {
let mut h = FakeHandle {
polls: vec![None].into(),
terminates: 0,
graceful_terminates: 0,
exit_on_graceful: true,
};
let (outcome, code) =
kill_on_deadline_with(&mut h, Instant::now(), Duration::from_millis(1));
assert_eq!((outcome, code), (KillOutcome::KilledOnDeadline, -15));
assert_eq!(h.graceful_terminates, 1);
assert_eq!(h.terminates, 0);
}
#[test]
fn deadline_kills_with_escalation_and_reaps() {
let mut h = FakeHandle {
polls: vec![None].into(),
terminates: 0,
graceful_terminates: 0,
exit_on_graceful: false,
};
let (outcome, code) = kill_on_deadline_with_grace(
&mut h,
Instant::now(),
Duration::from_millis(1),
Duration::from_millis(5),
);
assert_eq!((outcome, code), (KillOutcome::KilledOnDeadline, -9));
assert_eq!(h.graceful_terminates, 1);
assert_eq!(h.terminates, 1);
}
#[test]
fn expectation_matrix() {
#[cfg(target_os = "linux")]
{
assert_eq!(
CleanupExpectation::for_current_tier(Some("full")),
CleanupExpectation::Strong
);
assert!(matches!(
CleanupExpectation::for_current_tier(Some("fs-only")),
CleanupExpectation::BestEffort { .. }
));
}
#[cfg(target_os = "windows")]
{
assert_eq!(
CleanupExpectation::for_current_tier(None),
CleanupExpectation::Strong
);
}
#[cfg(target_os = "macos")]
{
assert!(matches!(
CleanupExpectation::for_current_tier(None),
CleanupExpectation::BestEffort { .. }
));
}
}
}