1use std::collections::VecDeque;
9use std::sync::{Arc, Condvar, Mutex};
10
11#[derive(Debug, Clone, PartialEq, serde::Serialize)]
14#[serde(tag = "event")]
15pub enum RunEvent {
16 RunStarted { run_id: String },
17 RunResumed { run_id: String },
18 NodeDispatched {
19 superstep: u64,
20 node: String,
21 task_path: String,
22 attempt: u32,
23 effect_seq: u32,
24 },
25 EffectSettled {
26 superstep: u64,
27 node: String,
28 task_path: String,
29 attempt: u32,
30 effect_seq: u32,
31 ok: bool,
32 },
33 AskRaised { superstep: u64, node: String, tool_call_id: String },
34 CheckpointWritten { superstep: u64, hash: String },
35 TokenChunk { node: String, task_path: String, attempt: u32, text: String },
38 RunFinished { run_id: String, outcome: String, dropped_events: u64 },
41}
42
43pub trait RunObserver: Send + Sync {
47 fn event(&self, ev: &RunEvent);
48}
49
50struct Queue {
51 buf: VecDeque<RunEvent>,
52 dropped: u64,
53 closed: bool,
54}
55
56struct Shared {
57 q: Mutex<Queue>,
58 cv: Condvar,
59}
60
61pub(crate) struct EventBus {
64 shared: Arc<Shared>,
65 capacity: usize,
66 worker: Option<std::thread::JoinHandle<()>>,
67}
68
69pub(crate) const EVENT_BUFFER: usize = 1024;
70
71impl EventBus {
72 pub fn new(observer: Arc<dyn RunObserver>, capacity: usize) -> Self {
73 let shared = Arc::new(Shared {
74 q: Mutex::new(Queue { buf: VecDeque::new(), dropped: 0, closed: false }),
75 cv: Condvar::new(),
76 });
77 let worker_shared = Arc::clone(&shared);
78 let worker = std::thread::spawn(move || loop {
79 let ev = {
82 let mut q = worker_shared.q.lock().unwrap_or_else(|e| e.into_inner());
83 loop {
84 if let Some(ev) = q.buf.pop_front() {
85 break ev;
86 }
87 if q.closed {
88 return;
89 }
90 q = worker_shared
91 .cv
92 .wait(q)
93 .unwrap_or_else(|e| e.into_inner());
94 }
95 };
96 observer.event(&ev);
97 });
98 EventBus { shared, capacity, worker: Some(worker) }
99 }
100
101 pub fn emit(&self, ev: RunEvent) {
104 let mut q = self.shared.q.lock().unwrap_or_else(|e| e.into_inner());
105 if q.buf.len() >= self.capacity {
106 q.buf.pop_front();
107 q.dropped += 1;
108 }
109 q.buf.push_back(ev);
110 drop(q);
111 self.shared.cv.notify_one();
112 }
113
114 pub fn dropped(&self) -> u64 {
115 self.shared.q.lock().unwrap_or_else(|e| e.into_inner()).dropped
116 }
117}
118
119impl Drop for EventBus {
120 fn drop(&mut self) {
121 {
122 let mut q = self.shared.q.lock().unwrap_or_else(|e| e.into_inner());
123 q.closed = true;
124 }
125 self.shared.cv.notify_one();
126 if let Some(w) = self.worker.take() {
127 let _ = w.join();
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::sync::mpsc;
136
137 struct Blocking {
140 entered: mpsc::Sender<()>,
141 release: Mutex<mpsc::Receiver<()>>,
142 seen: Mutex<Vec<RunEvent>>,
143 }
144
145 impl RunObserver for Blocking {
146 fn event(&self, ev: &RunEvent) {
147 let first = {
148 let mut seen = self.seen.lock().unwrap();
149 seen.push(ev.clone());
150 seen.len() == 1
151 };
152 if first {
153 let _ = self.entered.send(());
154 let _ = self.release.lock().unwrap().recv();
155 }
156 }
157 }
158
159 #[test]
160 fn bounded_buffer_drops_oldest_and_counts() {
161 let (entered_tx, entered_rx) = mpsc::channel();
162 let (release_tx, release_rx) = mpsc::channel();
163 let obs = Arc::new(Blocking {
164 entered: entered_tx,
165 release: Mutex::new(release_rx),
166 seen: Mutex::new(Vec::new()),
167 });
168 let bus = EventBus::new(Arc::clone(&obs) as Arc<dyn RunObserver>, 4);
169 let ev = |i: u64| RunEvent::CheckpointWritten { superstep: i, hash: String::new() };
170
171 bus.emit(ev(1));
173 entered_rx.recv().unwrap();
174 for i in 2..=10 {
176 bus.emit(ev(i));
177 }
178 assert_eq!(bus.dropped(), 5, "events 2..=6 were discarded, oldest first");
179 release_tx.send(()).unwrap();
180 drop(bus); let seen = obs.seen.lock().unwrap();
183 let got: Vec<u64> = seen
184 .iter()
185 .map(|e| match e {
186 RunEvent::CheckpointWritten { superstep, .. } => *superstep,
187 _ => unreachable!(),
188 })
189 .collect();
190 assert_eq!(got, vec![1, 7, 8, 9, 10], "in-flight + the newest four");
191 }
192}