use std::sync::Arc;
#[cfg(test)]
use std::sync::Mutex;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TrySendError};
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
}
}
#[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.published
.recv_timeout(timeout)
.expect("event publisher must reach the post-send gate");
self.observed
.send_timeout((), timeout)
.expect("event publisher must remain at the post-send gate");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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,
})
);
}
}