Skip to main content

roder_core/
bus.rs

1use std::collections::BTreeMap;
2use std::sync::{
3    Arc, Mutex,
4    atomic::{AtomicU64, Ordering},
5};
6
7use roder_api::events::{EventEnvelope, EventSource, RoderEvent, ThreadId, TurnId};
8use time::OffsetDateTime;
9use tokio::sync::broadcast;
10
11#[derive(Debug, Clone, Default)]
12pub struct EventFilter {
13    pub thread_id: Option<ThreadId>,
14    pub turn_id: Option<TurnId>,
15    pub kinds: Vec<String>,
16    pub sources: Vec<EventSource>,
17}
18
19impl EventFilter {
20    pub fn matches(&self, envelope: &EventEnvelope) -> bool {
21        if let Some(thread_id) = &self.thread_id
22            && envelope.thread_id.as_ref() != Some(thread_id)
23        {
24            return false;
25        }
26        if let Some(turn_id) = &self.turn_id
27            && envelope.turn_id.as_ref() != Some(turn_id)
28        {
29            return false;
30        }
31        if !self.kinds.is_empty() && !self.kinds.iter().any(|kind| kind == &envelope.kind) {
32            return false;
33        }
34        if !self.sources.is_empty() && !self.sources.iter().any(|source| source == &envelope.source)
35        {
36            return false;
37        }
38        true
39    }
40}
41
42#[derive(Clone)]
43pub struct EventBus {
44    sender: broadcast::Sender<EventEnvelope>,
45    next_seq: Arc<AtomicU64>,
46    publication: Arc<Mutex<Publication>>,
47}
48
49struct Publication {
50    next: u64,
51    ready: BTreeMap<u64, Option<EventEnvelope>>,
52}
53
54/// A cancelled persistence future must release its sequence slot too.
55pub(crate) struct PreparedEvent {
56    bus: EventBus,
57    envelope: EventEnvelope,
58    finished: bool,
59}
60
61impl PreparedEvent {
62    pub(crate) fn envelope(&self) -> &EventEnvelope {
63        &self.envelope
64    }
65
66    pub(crate) fn publish(mut self) {
67        self.finished = true;
68        self.bus
69            .finish(self.envelope.seq, Some(self.envelope.clone()));
70    }
71}
72
73impl Drop for PreparedEvent {
74    fn drop(&mut self) {
75        if !self.finished {
76            self.bus.finish(self.envelope.seq, None);
77        }
78    }
79}
80
81impl EventBus {
82    pub fn new(capacity: usize) -> Self {
83        let (sender, _) = broadcast::channel(capacity);
84        Self {
85            sender,
86            next_seq: Arc::new(AtomicU64::new(1)),
87            publication: Arc::new(Mutex::new(Publication {
88                next: 1,
89                ready: BTreeMap::new(),
90            })),
91        }
92    }
93
94    pub fn subscribe(&self) -> broadcast::Receiver<EventEnvelope> {
95        self.sender.subscribe()
96    }
97
98    pub fn emit(&self, event: RoderEvent) -> EventEnvelope {
99        let prepared = self.prepare(event);
100        let envelope = prepared.envelope().clone();
101        prepared.publish();
102        envelope
103    }
104
105    pub(crate) fn prepare(&self, event: RoderEvent) -> PreparedEvent {
106        PreparedEvent {
107            bus: self.clone(),
108            finished: false,
109            envelope: EventEnvelope {
110                event_id: uuid::Uuid::new_v4().to_string(),
111                seq: self.next_seq.fetch_add(1, Ordering::SeqCst),
112                timestamp: OffsetDateTime::now_utc(),
113                source: event.source(),
114                kind: event.kind().to_string(),
115                thread_id: event.thread_id().cloned(),
116                turn_id: event.turn_id().cloned(),
117                event,
118            },
119        }
120    }
121
122    fn finish(&self, sequence: u64, envelope: Option<EventEnvelope>) {
123        let mut publication = self
124            .publication
125            .lock()
126            .unwrap_or_else(|poisoned| poisoned.into_inner());
127        publication.ready.insert(sequence, envelope);
128        loop {
129            let next = publication.next;
130            let Some(envelope) = publication.ready.remove(&next) else {
131                break;
132            };
133            publication.next += 1;
134            if let Some(envelope) = envelope {
135                let _ = self.sender.send(envelope);
136            }
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use roder_api::events::{RoderEvent, TurnStarted};
145    use roder_api::inference::RuntimeProfile;
146
147    fn sample_event() -> RoderEvent {
148        RoderEvent::TurnStarted(TurnStarted {
149            thread_id: "thread".to_string(),
150            turn_id: "turn".to_string(),
151            runtime_profile: RuntimeProfile::Interactive,
152            timestamp: OffsetDateTime::now_utc(),
153        })
154    }
155
156    #[test]
157    fn prepared_events_preserve_order_and_abandoned_slots_do_not_block_subscribers() {
158        let bus = EventBus::new(16);
159        let mut receiver = bus.subscribe();
160        let first = bus.prepare(sample_event());
161        let second = bus.emit(sample_event());
162        assert!(receiver.try_recv().is_err());
163        let first_sequence = first.envelope().seq;
164        first.publish();
165        assert_eq!(receiver.try_recv().unwrap().seq, first_sequence);
166        assert_eq!(receiver.try_recv().unwrap().seq, second.seq);
167        let abandoned = bus.prepare(sample_event());
168        let following = bus.emit(sample_event());
169        assert!(receiver.try_recv().is_err());
170        drop(abandoned);
171        assert_eq!(receiver.try_recv().unwrap().seq, following.seq);
172    }
173
174    #[tokio::test]
175    async fn retains_burst_up_to_capacity_without_lagging() {
176        // A slow consumer (the TUI render loop only drains every ~166ms during
177        // an active turn) must be able to buffer a large burst of streaming
178        // events without the broadcast ring overflowing. This guards the
179        // capacity headroom that keeps tool/thinking rows from being dropped.
180        let capacity = 16_384;
181        let bus = EventBus::new(capacity);
182        let mut rx = bus.subscribe();
183
184        for _ in 0..capacity {
185            bus.emit(sample_event());
186        }
187
188        // Every buffered event is still readable; none were dropped.
189        for _ in 0..capacity {
190            assert!(rx.try_recv().is_ok());
191        }
192        assert!(matches!(
193            rx.try_recv(),
194            Err(tokio::sync::broadcast::error::TryRecvError::Empty)
195        ));
196    }
197
198    #[tokio::test]
199    async fn overflow_beyond_capacity_surfaces_lagged() {
200        // When the buffer truly overflows the consumer must still observe a
201        // `Lagged` signal so the TUI can record the drop and run its stuck-turn
202        // recovery instead of hanging.
203        let capacity = 16usize;
204        let bus = EventBus::new(capacity);
205        let mut rx = bus.subscribe();
206
207        for _ in 0..(capacity + 8) {
208            bus.emit(sample_event());
209        }
210
211        assert!(matches!(
212            rx.try_recv(),
213            Err(tokio::sync::broadcast::error::TryRecvError::Lagged(_))
214        ));
215    }
216}