use std::collections::VecDeque;
use std::num::NonZeroUsize;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, Weak};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::capability::CapabilityDenied;
use crate::component::ComponentId;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum LifecycleState {
Registered,
Starting,
Running,
Stopping,
Stopped,
Failed,
Removed,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct LifecycleEvent {
pub sequence: u64,
pub component_id: ComponentId,
pub kind: LifecycleEventKind,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum LifecycleEventKind {
Transition {
from: Option<LifecycleState>,
to: LifecycleState,
},
CapabilityDenied(CapabilityDenied),
FragmentContentUpdated {
key: crate::fragment::FragmentKey,
},
}
pub struct LifecycleSubscription {
queue: Arc<SubscriberQueue>,
}
impl LifecycleSubscription {
pub fn recv_timeout(&self, timeout: Duration) -> Result<LifecycleEvent, EventReceiveError> {
let guard = self
.queue
.state
.lock()
.map_err(|_| EventReceiveError::Poisoned)?;
let (mut guard, _wait) = self
.queue
.ready
.wait_timeout_while(guard, timeout, |state| {
state.events.is_empty() && !state.closed
})
.map_err(|_| EventReceiveError::Poisoned)?;
if let Some(event) = guard.events.pop_front() {
return Ok(event);
}
if guard.closed {
Err(EventReceiveError::Closed)
} else {
Err(EventReceiveError::Timeout)
}
}
pub fn try_recv(&self) -> Result<LifecycleEvent, EventTryReceiveError> {
let mut state = self
.queue
.state
.lock()
.map_err(|_| EventTryReceiveError::Poisoned)?;
if let Some(event) = state.events.pop_front() {
Ok(event)
} else if state.closed {
Err(EventTryReceiveError::Closed)
} else {
Err(EventTryReceiveError::Empty)
}
}
pub fn recv(&self) -> Result<LifecycleEvent, EventReceiveError> {
let guard = self
.queue
.state
.lock()
.map_err(|_| EventReceiveError::Poisoned)?;
let mut guard = self
.queue
.ready
.wait_while(guard, |state| state.events.is_empty() && !state.closed)
.map_err(|_| EventReceiveError::Poisoned)?;
if let Some(event) = guard.events.pop_front() {
return Ok(event);
}
Err(EventReceiveError::Closed)
}
#[must_use]
pub fn lagged_events(&self) -> usize {
self.queue.lagged.load(Ordering::Acquire)
}
#[must_use]
pub fn close_handle(&self) -> SubscriptionCloseHandle {
SubscriptionCloseHandle {
queue: Arc::downgrade(&self.queue),
}
}
}
pub struct SubscriptionCloseHandle {
queue: Weak<SubscriberQueue>,
}
impl SubscriptionCloseHandle {
pub fn close(&self) {
if let Some(queue) = self.queue.upgrade() {
if let Ok(mut state) = queue.state.lock() {
state.closed = true;
}
queue.ready.notify_all();
}
}
}
impl Drop for LifecycleSubscription {
fn drop(&mut self) {
if let Ok(mut state) = self.queue.state.lock() {
state.closed = true;
self.queue.ready.notify_all();
}
}
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum EventReceiveError {
#[error("lifecycle event receive timed out")]
Timeout,
#[error("lifecycle event subscription is closed")]
Closed,
#[error("lifecycle event subscription synchronization is poisoned")]
Poisoned,
}
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum EventTryReceiveError {
#[error("lifecycle event subscription is empty")]
Empty,
#[error("lifecycle event subscription is closed")]
Closed,
#[error("lifecycle event subscription synchronization is poisoned")]
Poisoned,
}
#[derive(Default)]
pub(crate) struct EventHub {
next_sequence: AtomicU64,
subscribers: Mutex<Vec<Weak<SubscriberQueue>>>,
#[cfg(test)]
pub(crate) constructed: AtomicUsize,
}
impl EventHub {
pub(crate) fn subscribe(
&self,
capacity: NonZeroUsize,
) -> Result<LifecycleSubscription, EventPublishError> {
let queue = Arc::new(SubscriberQueue {
capacity: capacity.get(),
state: Mutex::new(QueueState::default()),
ready: Condvar::new(),
lagged: AtomicUsize::new(0),
});
self.subscribers
.lock()
.map_err(|_| EventPublishError::Poisoned)?
.push(Arc::downgrade(&queue));
Ok(LifecycleSubscription { queue })
}
pub(crate) fn publish_transition(
&self,
component_id: ComponentId,
from: Option<LifecycleState>,
to: LifecycleState,
) -> Result<(), EventPublishError> {
self.publish(component_id, LifecycleEventKind::Transition { from, to })
}
pub(crate) fn publish_denial(&self, denial: CapabilityDenied) -> Result<(), EventPublishError> {
self.publish(
denial.component_id,
LifecycleEventKind::CapabilityDenied(denial),
)
}
pub(crate) fn publish_fragment_update(
&self,
key: crate::fragment::FragmentKey,
) -> Result<(), EventPublishError> {
self.publish(
key.component_id,
LifecycleEventKind::FragmentContentUpdated { key },
)
}
fn publish(
&self,
component_id: ComponentId,
kind: LifecycleEventKind,
) -> Result<(), EventPublishError> {
let mut subscribers = self
.subscribers
.lock()
.map_err(|_| EventPublishError::Poisoned)?;
subscribers.retain(|subscriber| subscriber.strong_count() > 0);
if subscribers.is_empty() {
return Ok(());
}
#[cfg(test)]
self.constructed.fetch_add(1, Ordering::AcqRel);
let event = LifecycleEvent {
sequence: self.next_sequence.fetch_add(1, Ordering::AcqRel),
component_id,
kind,
};
for subscriber in subscribers.iter().filter_map(Weak::upgrade) {
let mut state = subscriber
.state
.lock()
.map_err(|_| EventPublishError::Poisoned)?;
if state.events.len() == subscriber.capacity {
let _discarded = state.events.pop_front();
subscriber.lagged.fetch_add(1, Ordering::AcqRel);
}
state.events.push_back(event.clone());
subscriber.ready.notify_one();
}
Ok(())
}
}
#[derive(Debug, Error)]
pub(crate) enum EventPublishError {
#[error("lifecycle event stream synchronization is poisoned")]
Poisoned,
}
struct SubscriberQueue {
capacity: usize,
state: Mutex<QueueState>,
ready: Condvar,
lagged: AtomicUsize,
}
#[derive(Default)]
struct QueueState {
events: VecDeque<LifecycleEvent>,
closed: bool,
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use std::num::NonZeroUsize;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use super::{EventHub, EventReceiveError, LifecycleState};
use crate::component::ComponentId;
const HARNESS_WALL: Duration = Duration::from_secs(10);
fn capacity(value: usize) -> NonZeroUsize {
NonZeroUsize::new(value).unwrap_or(NonZeroUsize::MIN)
}
#[test]
fn blocked_recv_wakes_promptly_on_external_close() {
let hub = EventHub::default();
let subscription = hub
.subscribe(capacity(4))
.unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
let close = subscription.close_handle();
let (done, observed) = mpsc::channel();
let (entering, entered) = mpsc::channel();
let worker = thread::spawn(move || {
entering.send(()).ok();
let outcome = subscription.recv();
done.send(outcome).ok();
});
entered
.recv_timeout(HARNESS_WALL)
.unwrap_or_else(|_| panic!("receiver thread never started"));
thread::sleep(Duration::from_millis(100));
close.close();
let outcome = observed
.recv_timeout(HARNESS_WALL)
.unwrap_or_else(|_| panic!("blocked recv() never woke on external close"));
assert!(
matches!(outcome, Err(EventReceiveError::Closed)),
"external close must surface the typed Closed error, got: {outcome:?}"
);
worker
.join()
.unwrap_or_else(|_| panic!("receiver thread panicked"));
}
#[test]
fn no_subscriber_churn_constructs_zero_events() {
use std::sync::atomic::Ordering;
let hub = EventHub::default();
let id = ComponentId::derive("frame.test", "zero-cost");
for _ in 0..64 {
hub.publish_transition(id, None, LifecycleState::Registered)
.unwrap_or_else(|_| unreachable!("publish with no subscribers cannot be poisoned"));
}
assert_eq!(
hub.constructed.load(Ordering::Acquire),
0,
"zero live subscribers must cost zero event constructions"
);
let subscription = hub
.subscribe(capacity(4))
.unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
hub.publish_transition(id, None, LifecycleState::Registered)
.unwrap_or_else(|_| unreachable!("publish to a live subscriber cannot be poisoned"));
assert_eq!(hub.constructed.load(Ordering::Acquire), 1);
drop(subscription);
}
#[test]
fn recv_delivers_a_published_event_without_any_close() {
let hub = EventHub::default();
let subscription = hub
.subscribe(capacity(4))
.unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
let id = ComponentId::derive("frame.test", "event-close");
hub.publish_transition(id, None, LifecycleState::Registered)
.unwrap_or_else(|_| unreachable!("publish to a live subscriber cannot be poisoned"));
let event = subscription
.recv()
.unwrap_or_else(|error| panic!("recv with a queued event must succeed: {error}"));
assert_eq!(event.component_id, id);
}
#[test]
fn close_is_idempotent_and_still_typed_after_drop() {
let hub = EventHub::default();
let subscription = hub
.subscribe(capacity(4))
.unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
let close = subscription.close_handle();
close.close();
close.close();
assert!(matches!(
subscription.recv(),
Err(EventReceiveError::Closed)
));
drop(subscription);
close.close();
}
#[test]
fn close_handle_does_not_keep_the_queue_alive() {
let hub = EventHub::default();
let subscription = hub
.subscribe(capacity(1))
.unwrap_or_else(|_| unreachable!("subscribing to a fresh hub cannot be poisoned"));
let close = subscription.close_handle();
drop(subscription);
let id = ComponentId::derive("frame.test", "event-close-weak");
hub.publish_transition(id, None, LifecycleState::Registered)
.unwrap_or_else(|_| unreachable!("publish with no live subscriber is a no-op"));
assert!(close.queue.upgrade().is_none());
}
}