beamr/scheduler/
exit_events.rs1use std::sync::Arc;
4#[cfg(test)]
5use std::sync::Mutex;
6use std::sync::OnceLock;
7use std::sync::atomic::{AtomicBool, Ordering};
8use std::time::Duration;
9
10use crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TrySendError};
11
12use crate::process::ExitReason;
13
14pub const EXIT_EVENT_CAPACITY: usize = 1_024;
19
20#[derive(Copy, Clone, Debug, Eq, PartialEq)]
22pub enum ExitEvent {
23 Exited {
26 pid: u64,
28 reason: ExitReason,
30 },
31 Lagged,
38}
39
40#[derive(Copy, Clone, Debug, Eq, PartialEq)]
42pub enum ExitEventRecvError {
43 Disconnected,
45 Timeout,
47}
48
49impl std::fmt::Display for ExitEventRecvError {
50 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 formatter.write_str(match self {
52 Self::Disconnected => "exit-event publisher disconnected",
53 Self::Timeout => "timed out waiting for an exit event",
54 })
55 }
56}
57
58impl std::error::Error for ExitEventRecvError {}
59
60pub struct ExitEventSubscription {
66 receiver: Receiver<ExitEvent>,
67 overflowed: Arc<AtomicBool>,
68}
69
70impl ExitEventSubscription {
71 pub fn recv(&self) -> Result<ExitEvent, ExitEventRecvError> {
73 if self.take_lag_marker() {
74 return Ok(ExitEvent::Lagged);
75 }
76 self.receiver
77 .recv()
78 .map_err(|_| ExitEventRecvError::Disconnected)
79 }
80
81 pub fn recv_timeout(&self, timeout: Duration) -> Result<ExitEvent, ExitEventRecvError> {
83 if self.take_lag_marker() {
84 return Ok(ExitEvent::Lagged);
85 }
86 self.receiver
87 .recv_timeout(timeout)
88 .map_err(|error| match error {
89 RecvTimeoutError::Timeout => ExitEventRecvError::Timeout,
90 RecvTimeoutError::Disconnected => ExitEventRecvError::Disconnected,
91 })
92 }
93
94 fn take_lag_marker(&self) -> bool {
95 if !self.overflowed.swap(false, Ordering::AcqRel) {
96 return false;
97 }
98 for _ in 0..self.receiver.len() {
102 let _ = self.receiver.try_recv();
103 }
104 true
105 }
106}
107
108#[cfg(test)]
109#[derive(Clone)]
110struct ExitEventPublicationGate {
111 published: Sender<()>,
112 observed: Receiver<()>,
113}
114
115#[cfg(test)]
116pub(super) struct ExitEventPublicationObserver {
117 published: Receiver<()>,
118 observed: Sender<()>,
119}
120
121pub(super) struct ExitEventPublisher {
122 sender: OnceLock<Sender<ExitEvent>>,
123 overflowed: Arc<AtomicBool>,
124 capacity: usize,
125 #[cfg(test)]
126 publication_gate: Mutex<Option<ExitEventPublicationGate>>,
127}
128
129impl ExitEventPublisher {
130 pub(super) fn new() -> Self {
131 Self::with_capacity(EXIT_EVENT_CAPACITY)
132 }
133
134 fn with_capacity(capacity: usize) -> Self {
135 Self {
136 sender: OnceLock::new(),
137 overflowed: Arc::new(AtomicBool::new(false)),
138 capacity: capacity.max(1),
139 #[cfg(test)]
140 publication_gate: Mutex::new(None),
141 }
142 }
143
144 pub(super) fn subscribe(&self) -> Option<ExitEventSubscription> {
145 let (sender, receiver) = crossbeam_channel::bounded(self.capacity);
146 self.sender.set(sender).ok()?;
147 Some(ExitEventSubscription {
148 receiver,
149 overflowed: Arc::clone(&self.overflowed),
150 })
151 }
152
153 pub(super) fn publish(&self, event: ExitEvent) {
154 let Some(sender) = self.sender.get() else {
155 return;
156 };
157 match sender.try_send(event) {
158 Ok(()) => {
159 #[cfg(test)]
160 self.wait_at_publication_gate();
161 }
162 Err(TrySendError::Disconnected(_)) => {}
163 Err(TrySendError::Full(_)) => self.overflowed.store(true, Ordering::Release),
164 }
165 }
166
167 #[cfg(test)]
175 pub(super) fn install_publication_gate(&self) -> ExitEventPublicationObserver {
176 let (published, observe_publication) = crossbeam_channel::bounded(0);
177 let (observation_complete, observed) = crossbeam_channel::bounded(0);
178 let mut publication_gate = match self.publication_gate.lock() {
179 Ok(guard) => guard,
180 Err(poisoned) => poisoned.into_inner(),
181 };
182 *publication_gate = Some(ExitEventPublicationGate {
183 published,
184 observed,
185 });
186 ExitEventPublicationObserver {
187 published: observe_publication,
188 observed: observation_complete,
189 }
190 }
191
192 #[cfg(test)]
193 pub(super) fn clear_publication_gate(&self) {
194 let mut publication_gate = match self.publication_gate.lock() {
195 Ok(guard) => guard,
196 Err(poisoned) => poisoned.into_inner(),
197 };
198 *publication_gate = None;
199 }
200
201 #[cfg(test)]
202 fn wait_at_publication_gate(&self) {
203 let gate = match self.publication_gate.lock() {
204 Ok(guard) => guard.clone(),
205 Err(poisoned) => poisoned.into_inner().clone(),
206 };
207 let Some(gate) = gate else {
208 return;
209 };
210 if gate.published.send(()).is_ok() {
211 let _ = gate.observed.recv();
214 }
215 }
216}
217
218#[cfg(test)]
219impl ExitEventPublicationObserver {
220 pub(super) fn acknowledge_observed(&self, timeout: Duration) {
221 self.published
222 .recv_timeout(timeout)
223 .expect("event publisher must reach the post-send gate");
224 self.observed
225 .send_timeout((), timeout)
226 .expect("event publisher must remain at the post-send gate");
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn overflow_is_typed_and_queue_stays_bounded() {
236 let publisher = ExitEventPublisher::with_capacity(2);
237 let subscription = publisher.subscribe().expect("first subscriber");
238 for pid in 1..=3 {
239 publisher.publish(ExitEvent::Exited {
240 pid,
241 reason: ExitReason::Normal,
242 });
243 }
244
245 assert_eq!(subscription.recv(), Ok(ExitEvent::Lagged));
246 assert!(
247 publisher.subscribe().is_none(),
248 "subscription is single-use"
249 );
250 assert_eq!(
251 subscription.recv_timeout(Duration::ZERO),
252 Err(ExitEventRecvError::Timeout),
253 "lag resets the queued batch before recovery"
254 );
255 publisher.publish(ExitEvent::Exited {
256 pid: 4,
257 reason: ExitReason::Normal,
258 });
259 assert_eq!(
260 subscription.recv(),
261 Ok(ExitEvent::Exited {
262 pid: 4,
263 reason: ExitReason::Normal,
264 })
265 );
266 }
267}