Skip to main content

irtt_client/managed/
hub.rs

1use std::{
2    collections::{HashMap, VecDeque},
3    sync::{
4        atomic::{AtomicU64, Ordering},
5        Arc, Condvar, Mutex, Weak,
6    },
7};
8
9use crate::{error::EventSubscriptionError, event::ClientEvent};
10
11/// Configuration for one managed event subscriber.
12///
13/// Each subscriber has an independent bounded queue. The queue bounds memory
14/// growth when event production is faster than that subscriber's consumer.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct SubscriberConfig {
17    /// Maximum number of events buffered for this subscriber.
18    ///
19    /// Must be greater than zero.
20    pub capacity: usize,
21    /// Policy applied when publishing to a full subscriber queue.
22    pub overflow: SubscriberOverflow,
23}
24
25impl Default for SubscriberConfig {
26    fn default() -> Self {
27        Self {
28            capacity: 1024,
29            overflow: SubscriberOverflow::DropNewest,
30        }
31    }
32}
33
34/// Overflow behavior for a bounded event subscription queue.
35///
36/// These policies are applied independently per subscriber when that
37/// subscriber's queue is full.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum SubscriberOverflow {
40    /// Leave the existing queue unchanged and discard the newly published event.
41    DropNewest,
42    /// Remove the oldest queued event and enqueue the newly published event.
43    DropOldest,
44    /// Disconnect the subscriber and clear its queue.
45    Disconnect,
46}
47
48/// Publish/subscribe fan-out for [`ClientEvent`] values.
49///
50/// `EventHub` is used by managed sessions internally and is also exported for
51/// callers that want the same bounded subscription behavior around their own
52/// event producer. Publishing clones each event once per subscriber.
53#[derive(Debug, Clone)]
54pub struct EventHub {
55    inner: Arc<HubInner>,
56}
57
58#[derive(Debug)]
59struct HubInner {
60    next_id: AtomicU64,
61    subscribers: Mutex<HashMap<u64, Arc<SubscriberInner>>>,
62}
63
64/// Handle for receiving managed client events.
65///
66/// Each subscription has its own bounded queue configured by
67/// [`SubscriberConfig`]. The configured [`SubscriberOverflow`] policy applies
68/// when that queue is full. Dropping the handle unregisters the subscriber.
69#[must_use = "dropping the subscription unregisters it"]
70#[derive(Debug)]
71pub struct EventSubscription {
72    id: u64,
73    hub: Weak<HubInner>,
74    inner: Arc<SubscriberInner>,
75}
76
77#[derive(Debug)]
78struct SubscriberInner {
79    state: Mutex<SubscriberState>,
80    available: Condvar,
81    config: SubscriberConfig,
82}
83
84#[derive(Debug)]
85struct SubscriberState {
86    queue: VecDeque<ClientEvent>,
87    connected: bool,
88}
89
90impl EventHub {
91    /// Create an empty event hub with no subscribers.
92    pub fn new() -> Self {
93        Self {
94            inner: Arc::new(HubInner {
95                next_id: AtomicU64::new(1),
96                subscribers: Mutex::new(HashMap::new()),
97            }),
98        }
99    }
100
101    /// Register a new subscriber with its own bounded queue.
102    ///
103    /// Returns an error when `config.capacity` is zero.
104    pub fn subscribe(
105        &self,
106        config: SubscriberConfig,
107    ) -> Result<EventSubscription, crate::ClientError> {
108        if config.capacity == 0 {
109            return Err(crate::ClientError::InvalidConfig {
110                reason: "subscriber capacity must be greater than zero".to_owned(),
111            });
112        }
113
114        let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
115        let inner = Arc::new(SubscriberInner {
116            state: Mutex::new(SubscriberState {
117                queue: VecDeque::with_capacity(config.capacity),
118                connected: true,
119            }),
120            available: Condvar::new(),
121            config,
122        });
123        self.inner
124            .subscribers
125            .lock()
126            .expect("event hub mutex poisoned")
127            .insert(id, inner.clone());
128
129        Ok(EventSubscription {
130            id,
131            hub: Arc::downgrade(&self.inner),
132            inner,
133        })
134    }
135
136    /// Publish an event to all currently connected subscribers.
137    ///
138    /// Slow subscribers are handled according to their
139    /// [`SubscriberOverflow`] policy. Publishing does not block waiting for a
140    /// subscriber to consume queued events.
141    pub fn publish(&self, event: ClientEvent) {
142        let subscribers: Vec<(u64, Arc<SubscriberInner>)> = self
143            .inner
144            .subscribers
145            .lock()
146            .expect("event hub mutex poisoned")
147            .iter()
148            .map(|(id, subscriber)| (*id, subscriber.clone()))
149            .collect();
150
151        let mut disconnected = Vec::new();
152        for (id, subscriber) in subscribers {
153            // Events are cloned per subscriber; bounded queues keep slow
154            // consumers from causing unbounded memory growth.
155            if !subscriber.publish(event.clone()) {
156                disconnected.push(id);
157            }
158        }
159
160        if disconnected.is_empty() {
161            return;
162        }
163
164        let mut subscribers = self
165            .inner
166            .subscribers
167            .lock()
168            .expect("event hub mutex poisoned");
169        for id in disconnected {
170            subscribers.remove(&id);
171        }
172    }
173
174    /// Disconnect all subscribers after leaving their already queued events
175    /// available to drain.
176    pub fn disconnect_all(&self) {
177        let subscribers: Vec<Arc<SubscriberInner>> = self
178            .inner
179            .subscribers
180            .lock()
181            .expect("event hub mutex poisoned")
182            .drain()
183            .map(|(_, subscriber)| subscriber)
184            .collect();
185
186        for subscriber in subscribers {
187            subscriber.disconnect();
188        }
189    }
190
191    #[cfg(test)]
192    fn subscriber_count(&self) -> usize {
193        self.inner
194            .subscribers
195            .lock()
196            .expect("event hub mutex poisoned")
197            .len()
198    }
199}
200
201impl Default for EventHub {
202    fn default() -> Self {
203        Self::new()
204    }
205}
206
207impl EventSubscription {
208    /// Block until the next queued event is available.
209    ///
210    /// If the subscription is disconnected, this returns
211    /// [`EventSubscriptionError::Disconnected`] after any already queued events
212    /// have been drained.
213    pub fn recv(&self) -> Result<ClientEvent, EventSubscriptionError> {
214        let mut state = self.inner.state.lock().expect("subscriber mutex poisoned");
215        loop {
216            if let Some(event) = state.queue.pop_front() {
217                return Ok(event);
218            }
219            if !state.connected {
220                return Err(EventSubscriptionError::Disconnected);
221            }
222            state = self
223                .inner
224                .available
225                .wait(state)
226                .expect("subscriber mutex poisoned");
227        }
228    }
229
230    /// Try to receive one queued event without blocking.
231    ///
232    /// Returns `Ok(None)` when the subscription is still connected but no event
233    /// is queued. If the subscription is disconnected, this returns
234    /// [`EventSubscriptionError::Disconnected`] after any already queued events
235    /// have been drained.
236    pub fn try_recv(&self) -> Result<Option<ClientEvent>, EventSubscriptionError> {
237        let mut state = self.inner.state.lock().expect("subscriber mutex poisoned");
238        if let Some(event) = state.queue.pop_front() {
239            return Ok(Some(event));
240        }
241        if !state.connected {
242            return Err(EventSubscriptionError::Disconnected);
243        }
244        Ok(None)
245    }
246}
247
248impl Drop for EventSubscription {
249    fn drop(&mut self) {
250        self.inner.disconnect();
251        if let Some(hub) = self.hub.upgrade() {
252            hub.subscribers
253                .lock()
254                .expect("event hub mutex poisoned")
255                .remove(&self.id);
256        }
257    }
258}
259
260impl SubscriberInner {
261    fn publish(&self, event: ClientEvent) -> bool {
262        let mut state = self.state.lock().expect("subscriber mutex poisoned");
263        if !state.connected {
264            return false;
265        }
266
267        if state.queue.len() < self.config.capacity {
268            state.queue.push_back(event);
269            self.available.notify_one();
270            return true;
271        }
272
273        match self.config.overflow {
274            SubscriberOverflow::DropNewest => true,
275            SubscriberOverflow::DropOldest => {
276                state.queue.pop_front();
277                state.queue.push_back(event);
278                self.available.notify_one();
279                true
280            }
281            SubscriberOverflow::Disconnect => {
282                state.queue.clear();
283                state.connected = false;
284                self.available.notify_all();
285                false
286            }
287        }
288    }
289
290    fn disconnect(&self) {
291        let mut state = self.state.lock().expect("subscriber mutex poisoned");
292        state.connected = false;
293        self.available.notify_all();
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::{ClientTimestamp, WarningKind};
301
302    fn event(n: usize) -> ClientEvent {
303        ClientEvent::Warning {
304            kind: WarningKind::UntrackedReply,
305            message: format!("event-{n}"),
306            at: ClientTimestamp::now(),
307        }
308    }
309
310    fn event_message(event: ClientEvent) -> String {
311        match event {
312            ClientEvent::Warning { message, .. } => message,
313            other => panic!("unexpected event: {other:?}"),
314        }
315    }
316
317    fn assert_next(
318        sub: &EventSubscription,
319        expected: Result<Option<&str>, EventSubscriptionError>,
320    ) {
321        match (sub.try_recv(), expected) {
322            (Ok(Some(event)), Ok(Some(message))) => assert_eq!(event_message(event), message),
323            (Ok(None), Ok(None)) => {}
324            (Err(actual), Err(expected)) => assert_eq!(actual, expected),
325            (actual, expected) => panic!("expected {expected:?}, got {actual:?}"),
326        }
327    }
328
329    #[test]
330    fn publishes_to_multiple_subscribers() {
331        let hub = EventHub::new();
332        let a = hub.subscribe(SubscriberConfig::default()).unwrap();
333        let b = hub.subscribe(SubscriberConfig::default()).unwrap();
334
335        hub.publish(event(1));
336
337        assert_eq!(event_message(a.try_recv().unwrap().unwrap()), "event-1");
338        assert_eq!(event_message(b.try_recv().unwrap().unwrap()), "event-1");
339    }
340
341    #[test]
342    fn full_subscriber_overflow_policy_is_applied() {
343        let cases = [
344            (
345                SubscriberOverflow::DropNewest,
346                Ok(Some("event-1")),
347                Ok(None),
348            ),
349            (
350                SubscriberOverflow::DropOldest,
351                Ok(Some("event-2")),
352                Ok(None),
353            ),
354            (
355                SubscriberOverflow::Disconnect,
356                Err(EventSubscriptionError::Disconnected),
357                Err(EventSubscriptionError::Disconnected),
358            ),
359        ];
360
361        for (overflow, first_expected, second_expected) in cases {
362            let hub = EventHub::new();
363            let sub = hub
364                .subscribe(SubscriberConfig {
365                    capacity: 1,
366                    overflow,
367                })
368                .unwrap();
369
370            hub.publish(event(1));
371            hub.publish(event(2));
372
373            assert_next(&sub, first_expected);
374            assert_next(&sub, second_expected);
375        }
376    }
377
378    #[test]
379    fn full_subscriber_does_not_prevent_other_delivery() {
380        let hub = EventHub::new();
381        let slow = hub
382            .subscribe(SubscriberConfig {
383                capacity: 1,
384                overflow: SubscriberOverflow::DropNewest,
385            })
386            .unwrap();
387        let fast = hub
388            .subscribe(SubscriberConfig {
389                capacity: 4,
390                overflow: SubscriberOverflow::DropNewest,
391            })
392            .unwrap();
393
394        hub.publish(event(1));
395        hub.publish(event(2));
396
397        assert_eq!(event_message(slow.try_recv().unwrap().unwrap()), "event-1");
398        assert_eq!(event_message(fast.try_recv().unwrap().unwrap()), "event-1");
399        assert_eq!(event_message(fast.try_recv().unwrap().unwrap()), "event-2");
400    }
401
402    #[test]
403    fn subscribing_after_events_does_not_replay() {
404        let hub = EventHub::new();
405        hub.publish(event(1));
406
407        let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
408
409        assert!(sub.try_recv().unwrap().is_none());
410    }
411
412    #[test]
413    fn dropping_subscription_unregisters_from_hub() {
414        let hub = EventHub::new();
415        let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
416        assert_eq!(hub.subscriber_count(), 1);
417
418        drop(sub);
419
420        assert_eq!(hub.subscriber_count(), 0);
421    }
422
423    #[test]
424    fn disconnect_all_wakes_blocking_receivers() {
425        let hub = EventHub::new();
426        let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
427        hub.disconnect_all();
428
429        assert_eq!(
430            sub.recv().unwrap_err(),
431            EventSubscriptionError::Disconnected
432        );
433    }
434
435    #[test]
436    fn session_closed_event_can_be_queued() {
437        let hub = EventHub::new();
438        let sub = hub.subscribe(SubscriberConfig::default()).unwrap();
439        hub.publish(ClientEvent::SessionClosed {
440            remote: "127.0.0.1:1".parse().unwrap(),
441            token: 1,
442            at: ClientTimestamp::now(),
443        });
444
445        assert!(matches!(
446            sub.recv().unwrap(),
447            ClientEvent::SessionClosed { token: 1, .. }
448        ));
449    }
450}