use std::io;
use std::time::Duration;
use tokio::time::{Instant, sleep};
const POLL_INTERVAL: Duration = Duration::from_millis(20);
pub(crate) trait GracefulTarget {
fn signal_all(&self, signal: i32);
fn is_drained(&self) -> bool;
fn hard_kill(&self) -> io::Result<()>;
}
pub(crate) async fn run(
target: &impl GracefulTarget,
skip_drop_kill: &super::SkipDropKill,
signal: i32,
timeout: Duration,
escalate: bool,
) -> io::Result<()> {
target.signal_all(signal);
let deadline = Instant::now() + timeout.min(crate::MAX_DEADLINE);
while !target.is_drained() {
if Instant::now() >= deadline {
break;
}
sleep(POLL_INTERVAL).await;
}
if escalate && !target.is_drained() {
target.hard_kill()?;
} else if !escalate {
skip_drop_kill.request();
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
struct FakeTarget {
signals: AtomicUsize,
hard_kills: AtomicUsize,
alive_polls: AtomicUsize,
fail_hard_kill: bool,
}
impl FakeTarget {
fn new(alive_polls: usize) -> Self {
Self {
signals: AtomicUsize::new(0),
hard_kills: AtomicUsize::new(0),
alive_polls: AtomicUsize::new(alive_polls),
fail_hard_kill: false,
}
}
}
impl GracefulTarget for FakeTarget {
fn signal_all(&self, _signal: i32) {
self.signals.fetch_add(1, Ordering::Relaxed);
}
fn is_drained(&self) -> bool {
let remaining = self.alive_polls.load(Ordering::Relaxed);
if remaining == 0 {
return true;
}
self.alive_polls.store(remaining - 1, Ordering::Relaxed);
false
}
fn hard_kill(&self) -> io::Result<()> {
self.hard_kills.fetch_add(1, Ordering::Relaxed);
if self.fail_hard_kill {
Err(io::Error::other("hard_kill failed"))
} else {
Ok(())
}
}
}
#[tokio::test]
async fn drained_before_deadline_does_not_escalate() {
let target = FakeTarget::new(0); let skip = crate::sys::SkipDropKill::new();
run(&target, &skip, 15, Duration::from_secs(10), true)
.await
.expect("graceful run");
assert_eq!(target.signals.load(Ordering::Relaxed), 1, "signalled once");
assert_eq!(
target.hard_kills.load(Ordering::Relaxed),
0,
"no escalation"
);
assert!(!skip.is_set(), "escalate path leaves skip clear");
}
#[tokio::test(start_paused = true)]
async fn drains_mid_poll_does_not_escalate() {
let target = FakeTarget::new(3);
let skip = crate::sys::SkipDropKill::new();
run(&target, &skip, 15, Duration::from_secs(10), true)
.await
.expect("graceful run");
assert_eq!(
target.hard_kills.load(Ordering::Relaxed),
0,
"drained in time"
);
assert!(!skip.is_set());
}
#[tokio::test(start_paused = true)]
async fn deadline_elapses_after_polling_then_escalates() {
let target = FakeTarget::new(usize::MAX);
let skip = crate::sys::SkipDropKill::new();
run(&target, &skip, 15, Duration::from_millis(50), true)
.await
.expect("graceful run");
assert_eq!(
target.hard_kills.load(Ordering::Relaxed),
1,
"escalated after the deadline elapsed"
);
assert!(!skip.is_set());
}
#[tokio::test]
async fn not_drained_by_deadline_escalates_when_asked() {
let target = FakeTarget::new(usize::MAX);
let skip = crate::sys::SkipDropKill::new();
run(&target, &skip, 15, Duration::ZERO, true)
.await
.expect("graceful run");
assert_eq!(
target.hard_kills.load(Ordering::Relaxed),
1,
"escalated once"
);
assert!(!skip.is_set(), "escalation does not set skip");
}
#[tokio::test]
async fn not_drained_without_escalation_sets_skip_and_spares_survivors() {
let target = FakeTarget::new(usize::MAX);
let skip = crate::sys::SkipDropKill::new();
run(&target, &skip, 15, Duration::ZERO, false)
.await
.expect("graceful run");
assert_eq!(target.hard_kills.load(Ordering::Relaxed), 0, "no hard kill");
assert!(skip.is_set(), "skip set so Drop spares survivors");
}
#[tokio::test]
async fn hard_kill_error_propagates() {
let mut target = FakeTarget::new(usize::MAX);
target.fail_hard_kill = true;
let skip = crate::sys::SkipDropKill::new();
let err = run(&target, &skip, 15, Duration::ZERO, true)
.await
.expect_err("hard_kill failure surfaces");
assert_eq!(err.kind(), io::ErrorKind::Other);
assert!(!skip.is_set());
}
#[tokio::test]
async fn saturating_timeout_does_not_panic() {
let target = FakeTarget::new(0); let skip = crate::sys::SkipDropKill::new();
run(&target, &skip, 15, Duration::MAX, true)
.await
.expect("graceful run with saturating timeout");
}
}