1use serde_json::{Value, json};
30use std::collections::VecDeque;
31use std::sync::atomic::{AtomicUsize, Ordering};
32use std::sync::{Arc, Mutex};
33
34const BUS_CAPACITY: usize = 1024;
36pub const RING_BYTE_BUDGET: usize = 32 * 1024 * 1024;
42
43fn ring_byte_budget() -> usize {
47 static BUDGET: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
48 *BUDGET.get_or_init(|| {
49 std::env::var("OCSERVE_EVENT_RING_MB")
50 .ok()
51 .and_then(|v| v.parse::<usize>().ok())
52 .filter(|mb| *mb >= 1)
53 .map(|mb| mb * 1024 * 1024)
54 .unwrap_or(RING_BYTE_BUDGET)
55 })
56}
57
58struct RingState {
59 items: VecDeque<(u64, Arc<str>)>,
61 next_seq: u64,
63 total_bytes: usize,
64}
65
66pub struct EventBusInner {
67 ring: Mutex<RingState>,
68 head: tokio::sync::watch::Sender<u64>,
70 receivers: AtomicUsize,
71}
72
73#[derive(Clone)]
74pub struct EventBus {
75 inner: Arc<EventBusInner>,
76}
77
78#[derive(Debug, PartialEq, Eq)]
82pub enum RecvError {
83 Lagged(u64),
84 Closed,
85}
86
87#[derive(Debug, PartialEq, Eq)]
88pub enum TryRecvError {
89 Empty,
90 Lagged(u64),
91 Closed,
92}
93
94pub struct EventBusReceiver {
97 inner: Arc<EventBusInner>,
98 last_seq: u64,
99 pending: VecDeque<String>,
101 head_rx: tokio::sync::watch::Receiver<u64>,
102}
103
104impl EventBus {
105 pub fn new() -> Self {
106 let (head, _) = tokio::sync::watch::channel(0u64);
107 Self {
108 inner: Arc::new(EventBusInner {
109 ring: Mutex::new(RingState {
110 items: VecDeque::new(),
111 next_seq: 0,
112 total_bytes: 0,
113 }),
114 head,
115 receivers: AtomicUsize::new(0),
116 }),
117 }
118 }
119
120 pub fn subscriber_count(&self) -> usize {
122 self.inner.receivers.load(Ordering::Relaxed)
123 }
124
125 pub fn publish(&self, frame: Value) {
130 self.publish_str(frame.to_string());
131 }
132
133 pub fn publish_str(&self, bytes: String) {
139 let len = bytes.len();
140 let mut ring = self.inner.ring.lock().expect("event ring poisoned");
141 ring.next_seq += 1;
142 let seq = ring.next_seq;
143 ring.total_bytes += len;
144 ring.items.push_back((seq, Arc::from(bytes)));
145 while ring.items.len() > 1
148 && (ring.items.len() > BUS_CAPACITY || ring.total_bytes > ring_byte_budget())
149 {
150 let (_, evicted) = ring.items.pop_front().expect("non-empty");
151 ring.total_bytes = ring.total_bytes.saturating_sub(evicted.len());
152 ocserve_metrics::counter("ocserve_event_ring_evicted_total", 1);
153 }
154 let depth = ring.items.len() as i64;
155 let bytes_now = ring.total_bytes as i64;
156 drop(ring);
157 ocserve_metrics::gauge("ocserve_event_ring_depth", depth);
158 ocserve_metrics::gauge("ocserve_event_ring_bytes", bytes_now);
159 let _ = self.inner.head.send(seq);
160 }
161
162 pub fn subscribe(&self) -> EventBusReceiver {
164 let last_seq = self
165 .inner
166 .ring
167 .lock()
168 .expect("event ring poisoned")
169 .next_seq;
170 let head_rx = self.inner.head.subscribe();
171 self.inner.receivers.fetch_add(1, Ordering::Relaxed);
172 EventBusReceiver {
173 inner: self.inner.clone(),
174 last_seq,
175 pending: VecDeque::new(),
176 head_rx,
177 }
178 }
179}
180
181impl Default for EventBus {
182 fn default() -> Self {
183 Self::new()
184 }
185}
186
187impl EventBusReceiver {
188 fn drain(&mut self) -> Result<bool, RecvError> {
191 let (front, next) = {
192 let ring = self.inner.ring.lock().expect("event ring poisoned");
193 (
194 ring.items.front().map(|(s, _)| *s),
195 ring.items
196 .iter()
197 .find(|(s, _)| *s > self.last_seq)
198 .map(|(s, a)| (*s, a.clone())),
199 )
200 };
201 if let Some(front) = front
203 && front > self.last_seq + 1
204 {
205 return Err(RecvError::Lagged(front - self.last_seq - 1));
206 }
207 match next {
208 Some((s, frame)) => {
209 self.last_seq = s;
210 self.pending.push_back(frame.to_string());
213 Ok(true)
214 }
215 None => Ok(false),
216 }
217 }
218
219 pub async fn recv(&mut self) -> Result<String, RecvError> {
220 loop {
221 if let Some(f) = self.pending.pop_front() {
222 return Ok(f);
223 }
224 match self.drain() {
225 Ok(true) => continue, Err(e) => return Err(e),
227 Ok(false) => {}
228 }
229 if self.head_rx.changed().await.is_err() {
233 if let Ok(true) = self.drain() {
234 continue;
235 }
236 if let Some(f) = self.pending.pop_front() {
237 return Ok(f);
238 }
239 return Err(RecvError::Closed);
240 }
241 }
242 }
243
244 pub fn try_recv(&mut self) -> Result<String, TryRecvError> {
245 if let Some(f) = self.pending.pop_front() {
246 return Ok(f);
247 }
248 match self.drain() {
249 Ok(true) => Ok(self.pending.pop_front().expect("drained one")),
250 Ok(false) => Err(TryRecvError::Empty),
254 Err(RecvError::Lagged(n)) => Err(TryRecvError::Lagged(n)),
255 Err(RecvError::Closed) => Err(TryRecvError::Closed),
256 }
257 }
258}
259
260impl Drop for EventBusReceiver {
261 fn drop(&mut self) {
262 self.inner.receivers.fetch_sub(1, Ordering::Relaxed);
263 }
264}
265
266pub fn frame(directory: &str, event_type: &str, properties: Value) -> Value {
268 json!({
269 "directory": directory,
270 "project": "global",
271 "payload": {
272 "id": crate::ids::evt_id(),
273 "type": event_type,
274 "properties": normalize_props(event_type, properties)
275 }
276 })
277}
278
279pub fn normalize_props(event_type: &str, mut props: Value) -> Value {
288 let base = event_type.strip_suffix(".1").unwrap_or(event_type);
289 if base == "message.part.updated"
291 && props.get("time").is_none()
292 && let Value::Object(m) = &mut props
293 {
294 let now = std::time::SystemTime::now()
295 .duration_since(std::time::UNIX_EPOCH)
296 .map(|d| d.as_millis() as i64)
297 .unwrap_or(0);
298 m.insert("time".into(), json!(now));
299 }
300 props
301}
302
303pub fn sync_frame(
306 directory: &str,
307 event_type: &str,
308 properties: Value,
309 seq: u64,
310 session_id: &str,
311) -> Value {
312 json!({
313 "directory": directory,
314 "project": "global",
315 "payload": {
316 "type": "sync",
317 "syncEvent": {
318 "id": crate::ids::evt_id(),
319 "type": format!("{event_type}.1"),
320 "seq": seq,
321 "aggregateID": session_id,
322 "data": properties
323 }
324 }
325 })
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use std::time::Duration;
332
333 #[test]
334 fn frame_shape_matches_capture() {
335 let f = frame("/x", "session.idle", json!({"sessionID": "ses_1"}));
336 assert_eq!(f["directory"], "/x");
337 assert_eq!(f["project"], "global");
338 assert_eq!(f["payload"]["type"], "session.idle");
339 assert_eq!(f["payload"]["properties"]["sessionID"], "ses_1");
340 assert!(f["payload"]["id"].as_str().unwrap().starts_with("evt_"));
341 }
342
343 #[test]
347 fn normalize_injects_part_time() {
348 let p = normalize_props(
349 "message.part.updated",
350 json!({"sessionID": "ses_1", "part": {"id": "prt_1"}}),
351 );
352 assert!(p["time"].is_number(), "time injected: {p}");
353 let p2 = normalize_props(
355 "message.part.updated",
356 json!({"sessionID": "ses_1", "part": {}, "time": 7}),
357 );
358 assert_eq!(p2["time"], 7);
359 let p3 = normalize_props("message.part.updated.1", json!({"part": {}}));
361 assert!(p3["time"].is_number());
362 let p4 = normalize_props("session.idle", json!({"sessionID": "ses_1"}));
364 assert!(p4.get("time").is_none());
365 }
366
367 #[test]
368 fn sync_frame_shape_matches_capture() {
369 let f = sync_frame(
370 "/x",
371 "message.updated",
372 json!({"sessionID": "ses_1"}),
373 7,
374 "ses_1",
375 );
376 let se = &f["payload"]["syncEvent"];
377 assert_eq!(se["type"], "message.updated.1");
378 assert_eq!(se["seq"], 7);
379 assert_eq!(se["aggregateID"], "ses_1");
380 assert_eq!(se["data"]["sessionID"], "ses_1");
381 }
382
383 #[tokio::test]
384 async fn bus_delivers_and_bounds_by_count() {
385 let bus = EventBus::new();
386 let mut rx = bus.subscribe();
387 bus.publish(frame("/x", "session.idle", json!({})));
388 let got = rx.recv().await.unwrap();
389 let v: Value = serde_json::from_str(&got).unwrap();
390 assert_eq!(v["payload"]["type"], "session.idle");
391 for _ in 0..2000 {
393 bus.publish(frame("/x", "session.status", json!({})));
394 }
395 match rx.recv().await {
396 Ok(_) | Err(RecvError::Lagged(_)) => {}
397 e => panic!("unexpected bus result: {e:?}"),
398 }
399 }
400
401 #[tokio::test]
405 async fn ring_is_byte_bounded_not_just_count_bounded() {
406 let bus = EventBus::new();
407 let big = "x".repeat(16 * 1024 * 1024);
409 for i in 0..4 {
410 bus.publish(frame(
411 "/x",
412 "message.part.updated",
413 json!({"i": i, "big": big}),
414 ));
415 }
416 let ring = bus.inner.ring.lock().unwrap();
417 assert!(
418 ring.total_bytes <= RING_BYTE_BUDGET,
419 "ring bytes {} exceeded budget",
420 ring.total_bytes
421 );
422 assert!(
423 ring.items.len() < 4,
424 "oversize frames must evict: depth {}",
425 ring.items.len()
426 );
427 let last = ring.items.back().unwrap();
429 assert_eq!(last.0, ring.next_seq, "newest frame kept");
430 }
431
432 #[tokio::test]
435 async fn single_oversize_frame_admitted_alone() {
436 let bus = EventBus::new();
437 for i in 0..5 {
438 bus.publish(frame("/x", "t", json!({"i": i})));
439 }
440 let huge = "y".repeat(RING_BYTE_BUDGET + 1024);
441 bus.publish(frame("/x", "message.part.updated", json!({"big": huge})));
442 let ring = bus.inner.ring.lock().unwrap();
443 assert_eq!(
444 ring.items.len(),
445 1,
446 "everything else shed for the oversize frame"
447 );
448 assert!(ring.total_bytes > RING_BYTE_BUDGET);
449 }
450
451 #[tokio::test]
455 async fn publisher_never_blocks_on_stalled_receivers() {
456 let bus = EventBus::new();
457 let _slow: Vec<_> = (0..8).map(|_| bus.subscribe()).collect();
458 let t0 = std::time::Instant::now();
459 for _ in 0..5000 {
460 bus.publish(frame(
461 "/x",
462 "message.part.updated",
463 json!({"pad": "z".repeat(1024)}),
464 ));
465 }
466 assert!(
467 t0.elapsed() < Duration::from_secs(5),
468 "publish loop took {:?}",
469 t0.elapsed()
470 );
471 let ring = bus.inner.ring.lock().unwrap();
473 assert!(ring.items.len() <= BUS_CAPACITY);
474 assert!(ring.total_bytes <= RING_BYTE_BUDGET);
475 }
476
477 #[tokio::test]
478 async fn subscribe_only_sees_future_and_try_recv_roundtrips() {
479 let bus = EventBus::new();
480 bus.publish(frame("/x", "old", json!({})));
481 let mut rx = bus.subscribe();
482 assert!(
483 matches!(rx.try_recv(), Err(TryRecvError::Empty)),
484 "subscribe is future-only (broadcast parity)"
485 );
486 bus.publish(frame("/x", "new", json!({})));
487 let got = rx.try_recv().unwrap();
488 assert!(got.contains("\"type\": \"new\"") || got.contains("\"type\":\"new\""));
489 assert!(matches!(rx.try_recv(), Err(TryRecvError::Empty)));
490 assert_eq!(bus.subscriber_count(), 1);
491 drop(rx);
492 assert_eq!(bus.subscriber_count(), 0);
493 }
494}
495
496#[cfg(test)]
497mod unfold_tests {
498 use super::*;
499 use std::time::Duration;
500
501 #[tokio::test(start_paused = true)]
503 async fn heartbeat_emits_at_10s_and_stream_stays_open() {
504 let bus = EventBus::new();
505 let mut rx = bus.subscribe();
506 let mut next_hb = tokio::time::Instant::now() + Duration::from_secs(10);
507 let mut got_hb = false;
508 for _ in 0..3 {
509 let dur = next_hb.saturating_duration_since(tokio::time::Instant::now());
510 match tokio::time::timeout(dur.max(Duration::from_millis(1)), rx.recv()).await {
511 Ok(Ok(_frame)) => panic!("unexpected frame"),
512 Ok(Err(e)) => panic!("recv errored: {e:?}"),
513 Err(_elapsed) => {
514 got_hb = true;
515 next_hb += Duration::from_secs(10);
516 }
517 }
518 }
519 assert!(got_hb, "heartbeat fired");
520 bus.publish(frame("/x", "after", json!({})));
522 let got = tokio::time::timeout(Duration::from_secs(1), rx.recv())
523 .await
524 .expect("frame arrives")
525 .expect("not lagged");
526 assert!(got.contains("after"));
527 }
528}