1use std::collections::VecDeque;
4use std::num::NonZeroUsize;
5use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
6use std::sync::{Arc, Condvar, Mutex, Weak};
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::capability::CapabilityDenied;
13use crate::component::ComponentId;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
17pub enum LifecycleState {
18 Registered,
20 Starting,
22 Running,
24 Stopping,
26 Stopped,
28 Failed,
30 Removed,
32}
33
34#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
36pub struct LifecycleEvent {
37 pub sequence: u64,
39 pub component_id: ComponentId,
41 pub kind: LifecycleEventKind,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
47pub enum LifecycleEventKind {
48 Transition {
50 from: Option<LifecycleState>,
52 to: LifecycleState,
54 },
55 CapabilityDenied(CapabilityDenied),
57 FragmentContentUpdated {
62 key: crate::fragment::FragmentKey,
64 },
65}
66
67pub struct LifecycleSubscription {
69 queue: Arc<SubscriberQueue>,
70}
71
72impl LifecycleSubscription {
73 pub fn recv_timeout(&self, timeout: Duration) -> Result<LifecycleEvent, EventReceiveError> {
79 let guard = self
80 .queue
81 .state
82 .lock()
83 .map_err(|_| EventReceiveError::Poisoned)?;
84 let (mut guard, _wait) = self
85 .queue
86 .ready
87 .wait_timeout_while(guard, timeout, |state| {
88 state.events.is_empty() && !state.closed
89 })
90 .map_err(|_| EventReceiveError::Poisoned)?;
91 if let Some(event) = guard.events.pop_front() {
92 return Ok(event);
93 }
94 if guard.closed {
95 Err(EventReceiveError::Closed)
96 } else {
97 Err(EventReceiveError::Timeout)
98 }
99 }
100
101 pub fn try_recv(&self) -> Result<LifecycleEvent, EventTryReceiveError> {
107 let mut state = self
108 .queue
109 .state
110 .lock()
111 .map_err(|_| EventTryReceiveError::Poisoned)?;
112 if let Some(event) = state.events.pop_front() {
113 Ok(event)
114 } else if state.closed {
115 Err(EventTryReceiveError::Closed)
116 } else {
117 Err(EventTryReceiveError::Empty)
118 }
119 }
120
121 pub fn recv(&self) -> Result<LifecycleEvent, EventReceiveError> {
131 let guard = self
132 .queue
133 .state
134 .lock()
135 .map_err(|_| EventReceiveError::Poisoned)?;
136 let mut guard = self
137 .queue
138 .ready
139 .wait_while(guard, |state| state.events.is_empty() && !state.closed)
140 .map_err(|_| EventReceiveError::Poisoned)?;
141 if let Some(event) = guard.events.pop_front() {
142 return Ok(event);
143 }
144 Err(EventReceiveError::Closed)
145 }
146
147 #[must_use]
149 pub fn lagged_events(&self) -> usize {
150 self.queue.lagged.load(Ordering::Acquire)
151 }
152
153 #[must_use]
160 pub fn close_handle(&self) -> SubscriptionCloseHandle {
161 SubscriptionCloseHandle {
162 queue: Arc::downgrade(&self.queue),
163 }
164 }
165}
166
167pub struct SubscriptionCloseHandle {
169 queue: Weak<SubscriberQueue>,
170}
171
172impl SubscriptionCloseHandle {
173 pub fn close(&self) {
181 if let Some(queue) = self.queue.upgrade() {
182 if let Ok(mut state) = queue.state.lock() {
183 state.closed = true;
184 }
185 queue.ready.notify_all();
186 }
187 }
188}
189
190impl Drop for LifecycleSubscription {
191 fn drop(&mut self) {
192 if let Ok(mut state) = self.queue.state.lock() {
193 state.closed = true;
194 self.queue.ready.notify_all();
195 }
196 }
197}
198
199#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
201pub enum EventReceiveError {
202 #[error("lifecycle event receive timed out")]
204 Timeout,
205 #[error("lifecycle event subscription is closed")]
207 Closed,
208 #[error("lifecycle event subscription synchronization is poisoned")]
210 Poisoned,
211}
212
213#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
215pub enum EventTryReceiveError {
216 #[error("lifecycle event subscription is empty")]
218 Empty,
219 #[error("lifecycle event subscription is closed")]
221 Closed,
222 #[error("lifecycle event subscription synchronization is poisoned")]
224 Poisoned,
225}
226
227#[derive(Default)]
228pub(crate) struct EventHub {
229 next_sequence: AtomicU64,
230 subscribers: Mutex<Vec<Weak<SubscriberQueue>>>,
231 #[cfg(test)]
235 pub(crate) constructed: AtomicUsize,
236}
237
238impl EventHub {
239 pub(crate) fn subscribe(
240 &self,
241 capacity: NonZeroUsize,
242 ) -> Result<LifecycleSubscription, EventPublishError> {
243 let queue = Arc::new(SubscriberQueue {
244 capacity: capacity.get(),
245 state: Mutex::new(QueueState::default()),
246 ready: Condvar::new(),
247 lagged: AtomicUsize::new(0),
248 });
249 self.subscribers
250 .lock()
251 .map_err(|_| EventPublishError::Poisoned)?
252 .push(Arc::downgrade(&queue));
253 Ok(LifecycleSubscription { queue })
254 }
255
256 pub(crate) fn publish_transition(
257 &self,
258 component_id: ComponentId,
259 from: Option<LifecycleState>,
260 to: LifecycleState,
261 ) -> Result<(), EventPublishError> {
262 self.publish(component_id, LifecycleEventKind::Transition { from, to })
263 }
264
265 pub(crate) fn publish_denial(&self, denial: CapabilityDenied) -> Result<(), EventPublishError> {
266 self.publish(
267 denial.component_id,
268 LifecycleEventKind::CapabilityDenied(denial),
269 )
270 }
271
272 pub(crate) fn publish_fragment_update(
273 &self,
274 key: crate::fragment::FragmentKey,
275 ) -> Result<(), EventPublishError> {
276 self.publish(
277 key.component_id,
278 LifecycleEventKind::FragmentContentUpdated { key },
279 )
280 }
281
282 fn publish(
283 &self,
284 component_id: ComponentId,
285 kind: LifecycleEventKind,
286 ) -> Result<(), EventPublishError> {
287 let mut subscribers = self
288 .subscribers
289 .lock()
290 .map_err(|_| EventPublishError::Poisoned)?;
291 subscribers.retain(|subscriber| subscriber.strong_count() > 0);
292 if subscribers.is_empty() {
293 return Ok(());
294 }
295 #[cfg(test)]
296 self.constructed.fetch_add(1, Ordering::AcqRel);
297 let event = LifecycleEvent {
298 sequence: self.next_sequence.fetch_add(1, Ordering::AcqRel),
299 component_id,
300 kind,
301 };
302 for subscriber in subscribers.iter().filter_map(Weak::upgrade) {
303 let mut state = subscriber
304 .state
305 .lock()
306 .map_err(|_| EventPublishError::Poisoned)?;
307 if state.events.len() == subscriber.capacity {
308 let _discarded = state.events.pop_front();
309 subscriber.lagged.fetch_add(1, Ordering::AcqRel);
310 }
311 state.events.push_back(event.clone());
312 subscriber.ready.notify_one();
313 }
314 Ok(())
315 }
316}
317
318#[derive(Debug, Error)]
319pub(crate) enum EventPublishError {
320 #[error("lifecycle event stream synchronization is poisoned")]
321 Poisoned,
322}
323
324struct SubscriberQueue {
325 capacity: usize,
326 state: Mutex<QueueState>,
327 ready: Condvar,
328 lagged: AtomicUsize,
329}
330
331#[derive(Default)]
332struct QueueState {
333 events: VecDeque<LifecycleEvent>,
334 closed: bool,
335}
336
337#[cfg(test)]
338mod tests {
339 #![allow(clippy::panic)]
340
341 use std::num::NonZeroUsize;
342 use std::sync::mpsc;
343 use std::thread;
344 use std::time::Duration;
345
346 use super::{EventHub, EventReceiveError, LifecycleState};
347 use crate::component::ComponentId;
348
349 const HARNESS_WALL: Duration = Duration::from_secs(10);
352
353 fn capacity(value: usize) -> NonZeroUsize {
354 NonZeroUsize::new(value).unwrap_or(NonZeroUsize::MIN)
355 }
356
357 #[test]
358 fn blocked_recv_wakes_promptly_on_external_close() {
359 let hub = EventHub::default();
360 let subscription = hub
361 .subscribe(capacity(4))
362 .unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
363 let close = subscription.close_handle();
364 let (done, observed) = mpsc::channel();
365 let (entering, entered) = mpsc::channel();
366 let worker = thread::spawn(move || {
367 entering.send(()).ok();
368 let outcome = subscription.recv();
369 done.send(outcome).ok();
370 });
371 entered
372 .recv_timeout(HARNESS_WALL)
373 .unwrap_or_else(|_| panic!("receiver thread never started"));
374 thread::sleep(Duration::from_millis(100));
378 close.close();
379 let outcome = observed
380 .recv_timeout(HARNESS_WALL)
381 .unwrap_or_else(|_| panic!("blocked recv() never woke on external close"));
382 assert!(
383 matches!(outcome, Err(EventReceiveError::Closed)),
384 "external close must surface the typed Closed error, got: {outcome:?}"
385 );
386 worker
387 .join()
388 .unwrap_or_else(|_| panic!("receiver thread panicked"));
389 }
390
391 #[test]
392 fn no_subscriber_churn_constructs_zero_events() {
393 use std::sync::atomic::Ordering;
394 let hub = EventHub::default();
395 let id = ComponentId::derive("frame.test", "zero-cost");
396 for _ in 0..64 {
397 hub.publish_transition(id, None, LifecycleState::Registered)
398 .unwrap_or_else(|_| unreachable!("publish with no subscribers cannot be poisoned"));
399 }
400 assert_eq!(
405 hub.constructed.load(Ordering::Acquire),
406 0,
407 "zero live subscribers must cost zero event constructions"
408 );
409 let subscription = hub
412 .subscribe(capacity(4))
413 .unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
414 hub.publish_transition(id, None, LifecycleState::Registered)
415 .unwrap_or_else(|_| unreachable!("publish to a live subscriber cannot be poisoned"));
416 assert_eq!(hub.constructed.load(Ordering::Acquire), 1);
417 drop(subscription);
418 }
419
420 #[test]
421 fn recv_delivers_a_published_event_without_any_close() {
422 let hub = EventHub::default();
423 let subscription = hub
424 .subscribe(capacity(4))
425 .unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
426 let id = ComponentId::derive("frame.test", "event-close");
427 hub.publish_transition(id, None, LifecycleState::Registered)
428 .unwrap_or_else(|_| unreachable!("publish to a live subscriber cannot be poisoned"));
429 let event = subscription
430 .recv()
431 .unwrap_or_else(|error| panic!("recv with a queued event must succeed: {error}"));
432 assert_eq!(event.component_id, id);
433 }
434
435 #[test]
436 fn close_is_idempotent_and_still_typed_after_drop() {
437 let hub = EventHub::default();
438 let subscription = hub
439 .subscribe(capacity(4))
440 .unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
441 let close = subscription.close_handle();
442 close.close();
443 close.close();
444 assert!(matches!(
445 subscription.recv(),
446 Err(EventReceiveError::Closed)
447 ));
448 drop(subscription);
449 close.close();
450 }
451
452 #[test]
453 fn close_handle_does_not_keep_the_queue_alive() {
454 let hub = EventHub::default();
455 let subscription = hub
456 .subscribe(capacity(1))
457 .unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
458 let close = subscription.close_handle();
459 drop(subscription);
460 let id = ComponentId::derive("frame.test", "event-close-weak");
463 hub.publish_transition(id, None, LifecycleState::Registered)
464 .unwrap_or_else(|_| unreachable!("publish with no live subscriber is a no-op"));
465 assert!(close.queue.upgrade().is_none());
466 }
467}