tawasuyu

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1373Shell 116Rust 74Markdown 641915 Definiciones

⎇ main crates/hammerd/src/events.rs Rust · 92 líneas · 3.0 KB Historia Quién tocó qué crudo

takana etapa 4: los 10 crates de librería y el CLI pasan a takana-* 24bcf1783 · Sergio ·

1//! Bus de eventos in-process. Cualquier subsistema (watcher, init, supervisor futuro) puede
2//! `publish(event)` para que **todas** las conexiones del bus de agente abiertas reciban una
3//! copia. No hay tópicos: el filtrado lo hace el cliente.
4//!
5//! Implementación: lista de canales `mpsc::Sender<Event>`, uno por conexión. Cuando un
6//! `send` falla (el peer cerró), el slot se marca caído y se purga en la siguiente publicación.
7//!
8//! Concurrencia: un `Mutex` sobre el vector basta. La cadencia esperada es baja (mutaciones
9//! humanas y builds, no logs de tracing); cuando se vuelva crítico, sustituiremos por
10//! `tokio::broadcast` o un sharded broadcaster.
11
12use std::sync::mpsc::{self, Receiver, Sender};
13use std::sync::{Arc, Mutex};
14
15use takana_core::proto::Event;
16
17#[derive(Clone)]
18pub struct EventBus {
19    subs: Arc<Mutex<Vec<Sender<Event>>>>,
20}
21
22impl Default for EventBus {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl EventBus {
29    pub fn new() -> Self {
30        Self { subs: Arc::new(Mutex::new(Vec::new())) }
31    }
32
33    /// Registra una nueva suscripción. El `Receiver` lo consume el thread de la conexión;
34    /// cuando ese thread termina (peer cerró), el `Sender` se desreferencia y queda muerto
35    /// implícitamente. La purga real ocurre en `publish` (perezosa).
36    pub fn subscribe(&self) -> Receiver<Event> {
37        let (tx, rx) = mpsc::channel();
38        if let Ok(mut g) = self.subs.lock() {
39            g.push(tx);
40        }
41        rx
42    }
43
44    /// Difunde un evento a todos los suscriptores vivos. Los muertos (send falla) se
45    /// purgan en sitio.
46    pub fn publish(&self, ev: &Event) {
47        let Ok(mut g) = self.subs.lock() else { return };
48        g.retain(|tx| tx.send(ev.clone()).is_ok());
49    }
50
51    /// Sólo para tests / introspección.
52    #[cfg(test)]
53    pub fn live_subs(&self) -> usize {
54        self.subs.lock().map(|g| g.len()).unwrap_or(0)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use takana_core::proto::Event;
62
63    #[test]
64    fn publish_reaches_all_subs() {
65        let bus = EventBus::new();
66        let a = bus.subscribe();
67        let b = bus.subscribe();
68        bus.publish(&Event::Crashed { service: "web".into(), code: 1 });
69        assert!(matches!(a.try_recv().unwrap(), Event::Crashed { code: 1, .. }));
70        assert!(matches!(b.try_recv().unwrap(), Event::Crashed { code: 1, .. }));
71    }
72
73    #[test]
74    fn dead_subs_get_purged() {
75        let bus = EventBus::new();
76        let a = bus.subscribe();
77        let b = bus.subscribe();
78        assert_eq!(bus.live_subs(), 2);
79        drop(b); // simula desconexión del peer
80        bus.publish(&Event::Crashed { service: "x".into(), code: 0 });
81        // a sigue vivo; b se purgó al fallar el send.
82        assert!(a.try_recv().is_ok());
83        assert_eq!(bus.live_subs(), 1);
84    }
85
86    #[test]
87    fn no_subs_is_noop() {
88        let bus = EventBus::new();
89        bus.publish(&Event::Crashed { service: "x".into(), code: 0 });
90        // No panic, no nada.
91    }
92}