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(())
}
pub(crate) trait PidTarget {
fn signal(&self, signal: i32);
fn is_alive(&self) -> bool;
fn hard_kill(&self);
}
pub(crate) async fn run_pid(target: &impl PidTarget, signal: i32, grace: Duration) {
target.signal(signal);
let deadline = Instant::now() + grace.min(crate::MAX_DEADLINE);
loop {
let now = Instant::now();
if now >= deadline {
break; }
if !target.is_alive() {
return; }
sleep(POLL_INTERVAL.min(deadline - now)).await;
}
target.hard_kill();
}
#[cfg(unix)]
pub(crate) struct UnixChild(i32);
#[cfg(unix)]
impl UnixChild {
pub(crate) fn new(pid: i32) -> Self {
Self(pid)
}
}
#[cfg(unix)]
impl PidTarget for UnixChild {
fn signal(&self, signal: i32) {
unsafe {
libc::kill(self.0, signal);
}
}
fn is_alive(&self) -> bool {
let rc = unsafe { libc::kill(self.0, 0) };
rc == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
fn hard_kill(&self) {
unsafe {
libc::kill(self.0, libc::SIGKILL);
}
}
}
#[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");
}
struct FakePid {
signals: AtomicUsize,
last_signal: std::sync::atomic::AtomicI32,
hard_kills: AtomicUsize,
alive_polls: AtomicUsize,
}
impl FakePid {
fn new(alive_polls: usize) -> Self {
Self {
signals: AtomicUsize::new(0),
last_signal: std::sync::atomic::AtomicI32::new(0),
hard_kills: AtomicUsize::new(0),
alive_polls: AtomicUsize::new(alive_polls),
}
}
}
impl PidTarget for FakePid {
fn signal(&self, signal: i32) {
self.signals.fetch_add(1, Ordering::Relaxed);
self.last_signal.store(signal, Ordering::Relaxed);
}
fn is_alive(&self) -> bool {
let remaining = self.alive_polls.load(Ordering::Relaxed);
if remaining == 0 {
return false;
}
self.alive_polls.store(remaining - 1, Ordering::Relaxed);
true
}
fn hard_kill(&self) {
self.hard_kills.fetch_add(1, Ordering::Relaxed);
}
}
#[tokio::test(start_paused = true)]
async fn pid_child_that_catches_the_signal_is_still_hard_killed() {
let target = FakePid::new(usize::MAX);
run_pid(&target, 15, Duration::from_millis(100)).await;
assert_eq!(target.signals.load(Ordering::Relaxed), 1, "signalled once");
assert_eq!(
target.last_signal.load(Ordering::Relaxed),
15,
"the configured graceful signal is delivered, not a hard-coded one"
);
assert_eq!(
target.hard_kills.load(Ordering::Relaxed),
1,
"a survivor that rode out the grace is force-killed"
);
}
#[tokio::test(start_paused = true)]
async fn pid_child_that_exits_within_grace_skips_the_hard_kill() {
let target = FakePid::new(2);
run_pid(&target, 15, Duration::from_secs(10)).await;
assert_eq!(
target.hard_kills.load(Ordering::Relaxed),
0,
"a child gone within the grace is not force-killed (no recycled-pid SIGKILL)"
);
}
#[tokio::test]
async fn pid_saturating_grace_does_not_panic() {
let target = FakePid::new(0); run_pid(&target, 15, Duration::MAX).await;
assert_eq!(
target.hard_kills.load(Ordering::Relaxed),
0,
"an already-gone child is not force-killed"
);
}
}