use std::io;
use std::time::Duration;
#[cfg(unix)]
use std::sync::Arc;
#[cfg(unix)]
use super::pid_gate::PidGate;
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<()> {
let epoch = skip_drop_kill.begin_shutdown();
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(epoch);
}
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 {
gate: Arc<PidGate>,
}
#[cfg(unix)]
impl UnixChild {
pub(crate) fn new(gate: Arc<PidGate>) -> Self {
Self { gate }
}
}
#[cfg(unix)]
impl PidTarget for UnixChild {
fn signal(&self, signal: i32) {
self.gate.with_live_pid((), |pid| {
unsafe {
libc::kill(pid as i32, signal);
}
});
}
fn is_alive(&self) -> bool {
self.gate.with_live_pid(false, |pid| {
let rc = unsafe { libc::kill(pid as i32, 0) };
rc == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
})
}
fn hard_kill(&self) {
self.gate.with_live_pid((), |pid| {
unsafe {
libc::kill(pid as i32, 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(start_paused = true)]
async fn a_concurrent_rearm_wins_over_a_stale_non_escalating_request() {
struct RacingRearm<'a> {
latch: &'a crate::sys::SkipDropKill,
polls: AtomicUsize,
}
impl GracefulTarget for RacingRearm<'_> {
fn signal_all(&self, _signal: i32) {}
fn is_drained(&self) -> bool {
if self.polls.fetch_add(1, Ordering::Relaxed) == 1 {
self.latch.clear();
}
false
}
fn hard_kill(&self) -> io::Result<()> {
Ok(())
}
}
let skip = crate::sys::SkipDropKill::new();
skip.clear();
let target = RacingRearm {
latch: &skip,
polls: AtomicUsize::new(0),
};
run(&target, &skip, 15, Duration::from_millis(100), false)
.await
.expect("graceful run");
assert!(
!skip.is_set(),
"a spawn/adopt that re-armed mid-shutdown must not be re-spared by the \
shutdown's stale request — the fresh child keeps its Drop-kill backstop"
);
}
#[tokio::test(start_paused = true)]
async fn a_non_escalating_shutdown_without_a_race_still_spares() {
let target = FakeTarget::new(3); let skip = crate::sys::SkipDropKill::new();
skip.clear(); run(&target, &skip, 15, Duration::from_secs(10), false)
.await
.expect("graceful run");
assert!(
skip.is_set(),
"an unraced non-escalating shutdown spares its survivors on Drop"
);
}
#[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"
);
}
#[cfg(unix)]
#[test]
fn a_retired_unix_child_reports_gone_even_for_a_live_pid() {
let gate = std::sync::Arc::new(PidGate::new(Some(std::process::id())));
gate.retire();
let child = UnixChild::new(gate);
assert!(
!child.is_alive(),
"a retired pid must report gone, not probe the recycled pid alive"
);
let live_gate = std::sync::Arc::new(PidGate::new(Some(std::process::id())));
let live_child = UnixChild::new(live_gate);
assert!(
live_child.is_alive(),
"a live, un-retired pid still probes alive"
);
}
#[cfg(unix)]
#[tokio::test(start_paused = true)]
async fn run_pid_leaves_a_retired_live_pid_untouched() {
let gate = std::sync::Arc::new(PidGate::new(Some(std::process::id())));
gate.retire();
let target = UnixChild::new(gate);
let start = Instant::now();
run_pid(&target, 15, Duration::from_secs(10)).await;
assert!(
start.elapsed() < Duration::from_secs(1),
"a retired target ends the grace immediately, before any hard kill"
);
}
#[cfg(unix)]
#[tokio::test(start_paused = true)]
async fn a_reap_landing_mid_grace_stands_the_detached_kill_down() {
struct RetireAfterFirstPoll {
inner: UnixChild,
gate: std::sync::Arc<PidGate>,
signals: AtomicUsize,
polls: AtomicUsize,
hard_kills: AtomicUsize,
}
impl PidTarget for RetireAfterFirstPoll {
fn signal(&self, _signal: i32) {
self.signals.fetch_add(1, Ordering::SeqCst);
}
fn is_alive(&self) -> bool {
let alive = self.inner.is_alive();
if self.polls.fetch_add(1, Ordering::SeqCst) == 0 {
self.gate.retire();
}
alive
}
fn hard_kill(&self) {
self.hard_kills.fetch_add(1, Ordering::SeqCst);
self.inner.hard_kill();
}
}
let gate = std::sync::Arc::new(PidGate::new(Some(std::process::id())));
let target = RetireAfterFirstPoll {
inner: UnixChild::new(gate.clone()),
gate,
signals: AtomicUsize::new(0),
polls: AtomicUsize::new(0),
hard_kills: AtomicUsize::new(0),
};
let start = Instant::now();
run_pid(&target, 15, Duration::from_secs(10)).await;
assert_eq!(
target.signals.load(Ordering::SeqCst),
1,
"the driver issues the graceful signal once, up front, before it polls"
);
assert_eq!(
target.hard_kills.load(Ordering::SeqCst),
0,
"a reap landing mid-grace must suppress the final SIGKILL (no recycled-pid kill)"
);
assert!(
start.elapsed() < Duration::from_secs(1),
"the driver stands down on the next poll, not after riding out the grace"
);
}
}