#[derive(Debug)]
pub(crate) struct SkipDropKill(crate::sync::atomic::AtomicUsize);
impl Default for SkipDropKill {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ShutdownEpoch(usize);
impl SkipDropKill {
const SKIP_BIT: usize = 1;
const GEN_STEP: usize = Self::SKIP_BIT << 1;
pub(crate) fn new() -> Self {
Self(crate::sync::atomic::AtomicUsize::new(0))
}
pub(crate) fn begin_shutdown(&self) -> ShutdownEpoch {
use std::sync::atomic::Ordering;
ShutdownEpoch(self.0.load(Ordering::Acquire) & !Self::SKIP_BIT)
}
pub(crate) fn request(&self, epoch: ShutdownEpoch) {
use std::sync::atomic::Ordering;
let _ = self.0.compare_exchange(
epoch.0,
epoch.0 | Self::SKIP_BIT,
Ordering::Release,
Ordering::Relaxed,
);
}
pub(crate) fn clear(&self) {
use std::sync::atomic::Ordering;
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(_) => return,
Err(actual) => cur = actual,
}
}
}
pub(crate) fn is_set(&self) -> bool {
use std::sync::atomic::Ordering;
(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_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 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"
);
});
}
}