use std::sync::Arc;
#[cfg(test)]
use std::sync::Mutex;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TrySendError};
use dashmap::DashMap;
use crate::process::ExitReason;
pub const EXIT_EVENT_CAPACITY: usize = 1_024;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ExitEvent {
Exited {
pid: u64,
reason: ExitReason,
},
Lagged,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ExitEventRecvError {
Disconnected,
Timeout,
}
impl std::fmt::Display for ExitEventRecvError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::Disconnected => "exit-event publisher disconnected",
Self::Timeout => "timed out waiting for an exit event",
})
}
}
impl std::error::Error for ExitEventRecvError {}
pub struct ExitEventSubscription {
receiver: Receiver<ExitEvent>,
overflowed: Arc<AtomicBool>,
}
impl ExitEventSubscription {
pub fn recv(&self) -> Result<ExitEvent, ExitEventRecvError> {
if self.take_lag_marker() {
return Ok(ExitEvent::Lagged);
}
self.receiver
.recv()
.map_err(|_| ExitEventRecvError::Disconnected)
}
pub fn recv_timeout(&self, timeout: Duration) -> Result<ExitEvent, ExitEventRecvError> {
if self.take_lag_marker() {
return Ok(ExitEvent::Lagged);
}
self.receiver
.recv_timeout(timeout)
.map_err(|error| match error {
RecvTimeoutError::Timeout => ExitEventRecvError::Timeout,
RecvTimeoutError::Disconnected => ExitEventRecvError::Disconnected,
})
}
fn take_lag_marker(&self) -> bool {
if !self.overflowed.swap(false, Ordering::AcqRel) {
return false;
}
for _ in 0..self.receiver.len() {
let _ = self.receiver.try_recv();
}
true
}
}
#[derive(Debug)]
pub enum ExitWatchState {
Live(ExitWatch),
AlreadyExited(ExitReason),
NoRecord,
}
pub struct ExitWatch {
pid: u64,
watch_id: u64,
receiver: Receiver<(u64, ExitReason)>,
registry: Arc<ExitWatchRegistry>,
}
impl std::fmt::Debug for ExitWatch {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ExitWatch")
.field("pid", &self.pid)
.finish_non_exhaustive()
}
}
impl ExitWatch {
#[must_use]
pub fn pid(&self) -> u64 {
self.pid
}
pub fn recv(&self) -> Result<(u64, ExitReason), ExitEventRecvError> {
self.receiver
.recv()
.map_err(|_| ExitEventRecvError::Disconnected)
}
pub fn recv_timeout(&self, timeout: Duration) -> Result<(u64, ExitReason), ExitEventRecvError> {
self.receiver
.recv_timeout(timeout)
.map_err(|error| match error {
RecvTimeoutError::Timeout => ExitEventRecvError::Timeout,
RecvTimeoutError::Disconnected => ExitEventRecvError::Disconnected,
})
}
}
impl Drop for ExitWatch {
fn drop(&mut self) {
self.registry.deregister(self.pid, self.watch_id);
}
}
type WatchSlot = (u64, Sender<(u64, ExitReason)>);
pub(super) struct ExitWatchRegistry {
watches: DashMap<u64, Vec<WatchSlot>>,
next_watch_id: AtomicU64,
}
pub(super) enum StoreWatch {
Armed(ExitWatch),
AlreadyExited(ExitReason),
}
impl ExitWatchRegistry {
pub(super) fn new() -> Self {
Self {
watches: DashMap::new(),
next_watch_id: AtomicU64::new(0),
}
}
pub(super) fn register(self: &Arc<Self>, pid: u64) -> ExitWatch {
let watch_id = self.next_watch_id.fetch_add(1, Ordering::Relaxed);
let (sender, receiver) = crossbeam_channel::bounded(1);
self.watches
.entry(pid)
.or_default()
.push((watch_id, sender));
ExitWatch {
pid,
watch_id,
receiver,
registry: Arc::clone(self),
}
}
pub(super) fn fire(&self, pid: u64, reason: ExitReason) {
let Some((_pid, watchers)) = self.watches.remove(&pid) else {
return;
};
for (_watch_id, sender) in watchers {
let _ = sender.try_send((pid, reason));
}
}
fn deregister(&self, pid: u64, watch_id: u64) {
if let dashmap::mapref::entry::Entry::Occupied(mut entry) = self.watches.entry(pid) {
entry.get_mut().retain(|(id, _)| *id != watch_id);
if entry.get().is_empty() {
entry.remove();
}
}
}
#[cfg(test)]
pub(super) fn watched_pid_count(&self) -> usize {
self.watches.len()
}
}
#[cfg(test)]
#[derive(Clone)]
struct ExitEventPublicationGate {
published: Sender<()>,
observed: Receiver<()>,
}
#[cfg(test)]
pub(super) struct ExitEventPublicationObserver {
published: Receiver<()>,
observed: Sender<()>,
}
pub(super) struct ExitEventPublisher {
sender: OnceLock<Sender<ExitEvent>>,
overflowed: Arc<AtomicBool>,
capacity: usize,
#[cfg(test)]
publication_gate: Mutex<Option<ExitEventPublicationGate>>,
}
impl ExitEventPublisher {
pub(super) fn new() -> Self {
Self::with_capacity(EXIT_EVENT_CAPACITY)
}
fn with_capacity(capacity: usize) -> Self {
Self {
sender: OnceLock::new(),
overflowed: Arc::new(AtomicBool::new(false)),
capacity: capacity.max(1),
#[cfg(test)]
publication_gate: Mutex::new(None),
}
}
pub(super) fn subscribe(&self) -> Option<ExitEventSubscription> {
let (sender, receiver) = crossbeam_channel::bounded(self.capacity);
self.sender.set(sender).ok()?;
Some(ExitEventSubscription {
receiver,
overflowed: Arc::clone(&self.overflowed),
})
}
pub(super) fn publish(&self, event: ExitEvent) {
let Some(sender) = self.sender.get() else {
return;
};
match sender.try_send(event) {
Ok(()) => {
#[cfg(test)]
self.wait_at_publication_gate();
}
Err(TrySendError::Disconnected(_)) => {}
Err(TrySendError::Full(_)) => self.overflowed.store(true, Ordering::Release),
}
}
#[cfg(test)]
pub(super) fn install_publication_gate(&self) -> ExitEventPublicationObserver {
let (published, observe_publication) = crossbeam_channel::bounded(0);
let (observation_complete, observed) = crossbeam_channel::bounded(0);
let mut publication_gate = match self.publication_gate.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
*publication_gate = Some(ExitEventPublicationGate {
published,
observed,
});
ExitEventPublicationObserver {
published: observe_publication,
observed: observation_complete,
}
}
#[cfg(test)]
pub(super) fn clear_publication_gate(&self) {
let mut publication_gate = match self.publication_gate.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
*publication_gate = None;
}
#[cfg(test)]
fn wait_at_publication_gate(&self) {
let gate = match self.publication_gate.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
};
let Some(gate) = gate else {
return;
};
if gate.published.send(()).is_ok() {
let _ = gate.observed.recv();
}
}
}
#[cfg(test)]
impl ExitEventPublicationObserver {
pub(super) fn acknowledge_observed(&self, timeout: Duration) {
self.wait_for_publication(timeout);
self.release_publication(timeout);
}
pub(super) fn wait_for_publication(&self, timeout: Duration) {
self.published
.recv_timeout(timeout)
.expect("event publisher must reach the post-send gate");
}
pub(super) fn release_publication(&self, timeout: Duration) {
self.observed
.send_timeout((), timeout)
.expect("event publisher must remain at the post-send gate");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exclusivity_mechanism_is_still_the_oncelock() {
let source = include_str!("exit_events.rs");
assert!(
source.contains("sender: OnceLock<Sender<ExitEvent>>"),
"the exclusive subscription must keep its OnceLock mechanism; \
changing it re-opens aion's singleton-drainer contract and is a \
STOP, not a refactor"
);
}
#[test]
fn overflow_is_typed_and_queue_stays_bounded() {
let publisher = ExitEventPublisher::with_capacity(2);
let subscription = publisher.subscribe().expect("first subscriber");
for pid in 1..=3 {
publisher.publish(ExitEvent::Exited {
pid,
reason: ExitReason::Normal,
});
}
assert_eq!(subscription.recv(), Ok(ExitEvent::Lagged));
assert!(
publisher.subscribe().is_none(),
"subscription is single-use"
);
assert_eq!(
subscription.recv_timeout(Duration::ZERO),
Err(ExitEventRecvError::Timeout),
"lag resets the queued batch before recovery"
);
publisher.publish(ExitEvent::Exited {
pid: 4,
reason: ExitReason::Normal,
});
assert_eq!(
subscription.recv(),
Ok(ExitEvent::Exited {
pid: 4,
reason: ExitReason::Normal,
})
);
}
}