use crate::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug)]
pub(crate) struct SkipDropKill(AtomicUsize);
impl Default for SkipDropKill {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ShutdownEpoch(usize);
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct DisplacedSpare(Option<ShutdownEpoch>);
impl SkipDropKill {
const SKIP_BIT: usize = 1;
const GEN_STEP: usize = Self::SKIP_BIT << 1;
pub(crate) fn new() -> Self {
Self(AtomicUsize::new(0))
}
pub(crate) fn begin_shutdown(&self) -> ShutdownEpoch {
ShutdownEpoch(self.0.load(Ordering::Acquire) & !Self::SKIP_BIT)
}
pub(crate) fn request(&self, epoch: ShutdownEpoch) {
let _ = self.0.compare_exchange(
epoch.0,
epoch.0 | Self::SKIP_BIT,
Ordering::Release,
Ordering::Relaxed,
);
}
pub(crate) fn clear(&self) -> DisplacedSpare {
let mut cur = self.0.load(Ordering::Relaxed);
loop {
let next = cur.wrapping_add(Self::GEN_STEP) & !Self::SKIP_BIT;
match self
.0
.compare_exchange_weak(cur, next, Ordering::Release, Ordering::Relaxed)
{
Ok(_) => {
let displaced = (cur & Self::SKIP_BIT) != 0;
return DisplacedSpare(displaced.then_some(ShutdownEpoch(next)));
}
Err(actual) => cur = actual,
}
}
}
#[cfg_attr(any(windows, not(feature = "pty")), allow(dead_code))]
pub(crate) fn restore(&self, displaced: DisplacedSpare) {
if let Some(epoch) = displaced.0 {
self.request(epoch);
}
}
pub(crate) fn is_set(&self) -> bool {
(self.0.load(Ordering::Acquire) & Self::SKIP_BIT) != 0
}
}
#[cfg(all(test, not(loom)))]
mod skip_drop_kill_tests {
use super::SkipDropKill;
#[test]
fn request_then_clear_re_arms_the_drop_kill() {
let latch = SkipDropKill::new();
assert!(!latch.is_set(), "a fresh latch does not skip Drop's kill");
let epoch = latch.begin_shutdown();
latch.request(epoch);
assert!(latch.is_set(), "request() spares survivors on Drop");
latch.clear();
assert!(
!latch.is_set(),
"clear() re-arms Drop's kill for a reused group"
);
}
#[test]
fn a_stale_request_does_not_override_a_concurrent_clear() {
let latch = SkipDropKill::new();
latch.clear();
let epoch = latch.begin_shutdown();
latch.clear();
latch.request(epoch);
assert!(
!latch.is_set(),
"a stale non-escalating request must not re-spare a child that a \
concurrent spawn/adopt already re-armed the backstop for"
);
}
#[test]
fn request_spares_survivors_when_no_clear_intervenes() {
let latch = SkipDropKill::new();
latch.clear(); let epoch = latch.begin_shutdown();
latch.request(epoch);
assert!(
latch.is_set(),
"an unraced non-escalating request spares survivors"
);
latch.clear();
assert!(
!latch.is_set(),
"a spawn after the spare re-arms Drop's kill"
);
}
#[test]
fn a_rolled_back_spawn_restores_the_spare_its_clear_displaced() {
let latch = SkipDropKill::new();
let epoch = latch.begin_shutdown();
latch.request(epoch); let displaced = latch.clear(); assert!(!latch.is_set(), "the spawn's re-arm displaces the spare");
latch.restore(displaced); assert!(
latch.is_set(),
"a rolled-back spawn must restore the spare its re-arm displaced"
);
}
#[test]
fn a_rollback_creates_no_spare_its_clear_never_displaced() {
let latch = SkipDropKill::new();
let displaced = latch.clear(); latch.restore(displaced);
assert!(
!latch.is_set(),
"a rollback must not spare a group no shutdown ever spared"
);
}
#[test]
fn a_spawn_between_the_clear_and_the_restore_defeats_it() {
let latch = SkipDropKill::new();
let epoch = latch.begin_shutdown();
latch.request(epoch);
let displaced = latch.clear(); latch.clear(); latch.restore(displaced);
assert!(
!latch.is_set(),
"a spawn that re-armed the backstop after the rolled-back one must keep \
it armed — restoring the older spare would silently strip the newcomer"
);
}
#[test]
fn only_the_clear_that_displaced_the_spare_carries_it() {
let latch = SkipDropKill::new();
let epoch = latch.begin_shutdown();
latch.request(epoch);
let first = latch.clear();
let second = latch.clear();
latch.restore(second);
assert!(
!latch.is_set(),
"a clear that displaced no spare must restore none"
);
latch.restore(first);
assert!(
!latch.is_set(),
"and the clear that did displace it is by then out of date"
);
}
#[test]
fn a_stale_epoch_never_matches_a_later_generation() {
let latch = SkipDropKill::new();
let stale = latch.begin_shutdown();
latch.clear(); let fresh = latch.begin_shutdown();
latch.request(stale);
assert!(
!latch.is_set(),
"a stale epoch cannot spare at a newer generation"
);
latch.request(fresh);
assert!(
latch.is_set(),
"a current-generation request spares as usual"
);
}
}
#[cfg(all(test, loom))]
mod skip_drop_kill_loom_model {
use super::SkipDropKill;
use loom::sync::Arc;
#[test]
fn a_stale_request_never_re_spares_a_freshly_cleared_child() {
loom::model(|| {
let latch = Arc::new(SkipDropKill::new());
latch.clear();
let epoch = latch.begin_shutdown();
let shutdown = {
let latch = latch.clone();
loom::thread::spawn(move || latch.request(epoch))
};
latch.clear();
assert!(
!latch.is_set(),
"a stale non-escalating request re-spared a child that a fresh \
spawn/adopt had already re-armed the backstop for"
);
shutdown.join().unwrap();
assert!(
!latch.is_set(),
"the freshly re-armed backstop must survive the stale request"
);
});
}
#[test]
fn a_concurrent_spawn_defeats_a_rolled_back_spawns_restore() {
loom::model(|| {
let latch = Arc::new(SkipDropKill::new());
let epoch = latch.begin_shutdown();
latch.request(epoch);
let displaced = latch.clear();
let rollback = {
let latch = latch.clone();
loom::thread::spawn(move || latch.restore(displaced))
};
latch.clear();
rollback.join().unwrap();
assert!(
!latch.is_set(),
"a rolled-back spawn's restore re-spared a group a concurrent \
spawn/adopt had already re-armed the backstop for"
);
});
}
#[test]
fn concurrent_clears_leave_the_backstop_armed() {
loom::model(|| {
let latch = Arc::new(SkipDropKill::new());
let other = {
let latch = latch.clone();
loom::thread::spawn(move || latch.clear())
};
latch.clear();
other.join().unwrap();
assert!(
!latch.is_set(),
"two racing re-arms must leave Drop's kill armed"
);
});
}
}