use crate::process::ExitReason;
use dashmap::DashMap;
use std::collections::VecDeque;
use std::sync::Mutex;
pub(super) const TOMBSTONE_CAPACITY: usize = 65_536;
pub(super) struct BoundedTombstones {
reasons: DashMap<u64, ExitReason>,
order: Mutex<VecDeque<u64>>,
capacity: usize,
}
impl BoundedTombstones {
pub(super) fn new() -> Self {
Self::with_capacity(TOMBSTONE_CAPACITY)
}
pub(super) fn with_capacity(capacity: usize) -> Self {
Self {
reasons: DashMap::new(),
order: Mutex::new(VecDeque::new()),
capacity: capacity.max(1),
}
}
pub(super) fn get(&self, pid: &u64) -> Option<ExitReason> {
self.reasons.get(pid).map(|entry| *entry)
}
pub(super) fn contains_key(&self, pid: &u64) -> bool {
self.reasons.contains_key(pid)
}
pub(super) fn insert(&self, pid: u64, reason: ExitReason) -> Option<u64> {
if self.reasons.insert(pid, reason).is_some() {
return None;
}
let mut order = match self.order.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
order.push_back(pid);
if order.len() <= self.capacity {
return None;
}
while let Some(oldest) = order.pop_front() {
if let Some((evicted, _)) = self.reasons.remove(&oldest) {
return Some(evicted);
}
}
None
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.reasons.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_over_cap_stays_bounded() {
let cap = 8;
let store = BoundedTombstones::with_capacity(cap);
for pid in 0..1_000u64 {
store.insert(pid, ExitReason::Normal);
assert!(
store.len() <= cap,
"len {} exceeded cap {} after inserting pid {}",
store.len(),
cap,
pid
);
}
assert_eq!(store.len(), cap, "store settles exactly at the cap");
}
#[test]
fn most_recent_survive_and_are_readable() {
let cap = 8;
let store = BoundedTombstones::with_capacity(cap);
for pid in 0..100u64 {
let reason = if pid % 2 == 0 {
ExitReason::Normal
} else {
ExitReason::Kill
};
store.insert(pid, reason);
}
for pid in 92..100u64 {
let expected = if pid % 2 == 0 {
ExitReason::Normal
} else {
ExitReason::Kill
};
assert_eq!(
store.get(&pid),
Some(expected),
"recent pid {pid} must survive with its reason"
);
assert!(store.contains_key(&pid));
}
}
#[test]
fn oldest_are_evicted_recent_retained() {
let cap = 4;
let store = BoundedTombstones::with_capacity(cap);
for pid in 0..10u64 {
store.insert(pid, ExitReason::Normal);
}
for pid in 0..6u64 {
assert_eq!(store.get(&pid), None, "old pid {pid} must be evicted");
assert!(!store.contains_key(&pid));
}
for pid in 6..10u64 {
assert_eq!(
store.get(&pid),
Some(ExitReason::Normal),
"recent pid {pid} must be retained"
);
}
}
#[test]
fn overwrite_does_not_duplicate_or_misevict() {
let cap = 3;
let store = BoundedTombstones::with_capacity(cap);
store.insert(1, ExitReason::Normal);
store.insert(2, ExitReason::Normal);
store.insert(3, ExitReason::Normal);
store.insert(1, ExitReason::Kill);
assert_eq!(store.get(&1), Some(ExitReason::Kill));
assert_eq!(store.len(), cap);
store.insert(4, ExitReason::Normal);
assert_eq!(store.get(&1), None, "first-inserted pid is the one evicted");
assert_eq!(store.get(&2), Some(ExitReason::Normal));
assert_eq!(store.get(&3), Some(ExitReason::Normal));
assert_eq!(store.get(&4), Some(ExitReason::Normal));
assert_eq!(store.len(), cap);
}
}