tawasuyu

tawasuyu/brahman

Rust 572TOML 217Markdown 68JSON 198326 Definiciones

⎇ d8727a3038c45b4fc69a37fc988e38408ddb5f6c crates/modules/shipote/shipote-core/src/flow_channel.rs Rust · 401 líneas · 14.4 KB Historia Quién tocó qué crudo

feat(shipote): CPU% + pipeline live-tail + replay por bytes (fase J) d8727a303 · sergio ·

1//! Flow channels: data plane sobre Unix socket por edge enriquecido.
2//!
3//! Cuando un splitter detecta el TypeRef de un edge, además de replicar a
4//! los consumers internos del pipeline, se levanta un FlowChannel que
5//! expone los bytes a subscribers externos (otros módulos del fractal).
6//!
7//! ## Diseño
8//!
9//! - `tokio::sync::broadcast::channel` para fan-out lock-less entre el
10//!   splitter (sender) y los N subscribers conectados.
11//! - `UnixListener` accept-loop: por cada cliente nuevo, spawn una task
12//!   que drena el receiver y escribe al socket.
13//! - Subscribers lentos pueden perder mensajes (broadcast::Receiver::Lagged)
14//!   — se loguea warn y se sigue. Esto es deliberado para no bloquear el
15//!   splitter en consumers lentos.
16//!
17//! ## Lifetime
18//!
19//! `FlowChannel` se construye con `new(path)`. Cuando se drop:
20//! - El `accept_task` se cancela (vía drop del `tokio::task::JoinHandle`
21//!   que tenemos abort-on-drop).
22//! - El socket file se borra del FS (`Drop` impl).
23//!
24//! Sender clones son baratos; los subscribers conectados se enteran del
25//! cierre cuando todos los senders se dropean.
26
27use std::collections::VecDeque;
28use std::path::{Path, PathBuf};
29use std::sync::{Arc, Mutex};
30use tokio::io::AsyncWriteExt;
31use tokio::net::UnixListener;
32use tokio::sync::broadcast;
33use tokio::task::AbortHandle;
34use tracing::{debug, warn};
35
36/// Capacidad del broadcast channel. Si un subscriber está más de N chunks
37/// atrasado, queda `Lagged` y empieza a perder mensajes.
38const BROADCAST_CAP: usize = 64;
39
40/// Chunks default del replay buffer. Cuando un cliente nuevo se conecta,
41/// recibe hasta estos N chunks antes de iniciar el broadcast live.
42/// Override via `FlowChannel::with_replay_cap`.
43pub const DEFAULT_REPLAY_CHUNKS: usize = 32;
44
45pub struct FlowChannel {
46    sender: broadcast::Sender<Arc<Vec<u8>>>,
47    replay: Arc<Mutex<VecDeque<Arc<Vec<u8>>>>>,
48    replay_caps: ReplayCaps,
49    socket_path: PathBuf,
50    _accept_handle: AbortOnDrop,
51}
52
53#[derive(Debug, Clone, Copy)]
54pub struct ReplayCaps {
55    /// Máximo de chunks retenidos.
56    pub chunks: usize,
57    /// Máximo de bytes (sumando len de chunks). `0` = sin tope.
58    pub bytes: usize,
59}
60
61impl ReplayCaps {
62    pub fn chunks_only(chunks: usize) -> Self {
63        Self { chunks: chunks.max(1), bytes: 0 }
64    }
65    pub fn new(chunks: usize, bytes: usize) -> Self {
66        Self { chunks: chunks.max(1), bytes }
67    }
68}
69
70#[derive(Clone)]
71pub struct FlowSender {
72    sender: broadcast::Sender<Arc<Vec<u8>>>,
73    replay: Arc<Mutex<VecDeque<Arc<Vec<u8>>>>>,
74    replay_caps: ReplayCaps,
75}
76
77impl FlowSender {
78    /// Pushea al broadcast y al replay buffer. Si no hay subscribers,
79    /// el broadcast::send retorna Err pero igual guardamos en replay
80    /// (subscribers tarde verán los chunks pasados).
81    pub fn send(&self, data: Arc<Vec<u8>>) {
82        let incoming = data.len();
83        let caps = self.replay_caps;
84        if let Ok(mut g) = self.replay.lock() {
85            evict_for_incoming(&mut g, caps, incoming);
86            g.push_back(data.clone());
87        }
88        let _ = self.sender.send(data);
89    }
90}
91
92/// Evict los chunks más viejos para hacer espacio a un chunk entrante de
93/// `incoming` bytes — el buffer post-push queda dentro de los caps.
94fn evict_for_incoming(buf: &mut VecDeque<Arc<Vec<u8>>>, caps: ReplayCaps, incoming: usize) {
95    // 1) chunks: dejar lugar para 1 más.
96    while buf.len() + 1 > caps.chunks {
97        if buf.pop_front().is_none() {
98            break;
99        }
100    }
101    // 2) bytes (si está activado).
102    if caps.bytes > 0 {
103        let mut current: usize = buf.iter().map(|a| a.len()).sum();
104        while current + incoming > caps.bytes {
105            match buf.pop_front() {
106                Some(c) => current = current.saturating_sub(c.len()),
107                None => break,
108            }
109        }
110    }
111}
112
113impl std::fmt::Debug for FlowChannel {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.debug_struct("FlowChannel")
116            .field("socket_path", &self.socket_path)
117            .field("subscribers", &self.sender.receiver_count())
118            .finish()
119    }
120}
121
122impl FlowChannel {
123    /// Crea un FlowChannel atado al path `socket_path`. Si el path ya
124    /// existe, lo borra antes de bind (asume restart limpio).
125    pub fn new(socket_path: PathBuf) -> std::io::Result<Self> {
126        Self::with_replay_caps(socket_path, ReplayCaps::chunks_only(DEFAULT_REPLAY_CHUNKS))
127    }
128
129    pub fn with_replay_cap(socket_path: PathBuf, chunks: usize) -> std::io::Result<Self> {
130        Self::with_replay_caps(socket_path, ReplayCaps::chunks_only(chunks))
131    }
132
133    pub fn with_replay_caps(socket_path: PathBuf, caps: ReplayCaps) -> std::io::Result<Self> {
134        if socket_path.exists() {
135            let _ = std::fs::remove_file(&socket_path);
136        }
137        if let Some(parent) = socket_path.parent() {
138            let _ = std::fs::create_dir_all(parent);
139        }
140        let listener = UnixListener::bind(&socket_path)?;
141        let (tx, _rx_unused) = broadcast::channel::<Arc<Vec<u8>>>(BROADCAST_CAP);
142        let replay: Arc<Mutex<VecDeque<Arc<Vec<u8>>>>> =
143            Arc::new(Mutex::new(VecDeque::with_capacity(caps.chunks)));
144        let tx_for_accept = tx.clone();
145        let replay_for_accept = replay.clone();
146        let path_for_log = socket_path.clone();
147
148        let join = tokio::spawn(async move {
149            debug!(path = %path_for_log.display(), "flow channel listening");
150            loop {
151                let (mut stream, _addr) = match listener.accept().await {
152                    Ok(p) => p,
153                    Err(e) => {
154                        warn!(?e, "flow channel accept failed");
155                        return;
156                    }
157                };
158                // Snapshot del replay buffer Y subscribe al broadcast.
159                // El orden es crítico: subscribe ANTES de drenar el replay
160                // para no perder chunks que llegan justo en el medio.
161                let mut rx = tx_for_accept.subscribe();
162                let snapshot: Vec<Arc<Vec<u8>>> = {
163                    let g = replay_for_accept.lock().expect("replay lock");
164                    g.iter().cloned().collect()
165                };
166                tokio::spawn(async move {
167                    // Fase 1: drenar replay snapshot al subscriber.
168                    for chunk in &snapshot {
169                        if let Err(e) = stream.write_all(chunk).await {
170                            debug!(?e, "flow subscriber dropped during replay");
171                            return;
172                        }
173                    }
174                    // Fase 2: live broadcast.
175                    loop {
176                        match rx.recv().await {
177                            Ok(chunk) => {
178                                if let Err(e) = stream.write_all(&chunk).await {
179                                    debug!(?e, "flow subscriber dropped");
180                                    return;
181                                }
182                            }
183                            Err(broadcast::error::RecvError::Closed) => return,
184                            Err(broadcast::error::RecvError::Lagged(n)) => {
185                                warn!(skipped = n, "flow subscriber lagged");
186                            }
187                        }
188                    }
189                });
190            }
191        });
192
193        Ok(Self {
194            sender: tx,
195            replay,
196            replay_caps: caps,
197            socket_path,
198            _accept_handle: AbortOnDrop(join.abort_handle()),
199        })
200    }
201
202    /// Push un chunk al channel. Si no hay subscribers, drop silencioso.
203    /// Siempre se guarda en el replay buffer (con cap rotation por chunks
204    /// y opcionalmente por bytes).
205    pub fn send(&self, data: Vec<u8>) {
206        let incoming = data.len();
207        let arc = Arc::new(data);
208        let caps = self.replay_caps;
209        if let Ok(mut g) = self.replay.lock() {
210            evict_for_incoming(&mut g, caps, incoming);
211            g.push_back(arc.clone());
212        }
213        let _ = self.sender.send(arc);
214    }
215
216    pub fn socket_path(&self) -> &Path {
217        &self.socket_path
218    }
219
220    /// Handle clone-able para que tasks externas (splitter) pushen al
221    /// channel sin tener ownership del FlowChannel. Cada push se guarda
222    /// también en el replay buffer.
223    pub fn sender_handle(&self) -> FlowSender {
224        FlowSender {
225            sender: self.sender.clone(),
226            replay: self.replay.clone(),
227            replay_caps: self.replay_caps,
228        }
229    }
230
231    pub fn subscriber_count(&self) -> usize {
232        self.sender.receiver_count()
233    }
234}
235
236impl Drop for FlowChannel {
237    fn drop(&mut self) {
238        // El AbortOnDrop cancela el accept loop; sólo nos queda limpiar el
239        // socket file.
240        let _ = std::fs::remove_file(&self.socket_path);
241    }
242}
243
244struct AbortOnDrop(AbortHandle);
245impl Drop for AbortOnDrop {
246    fn drop(&mut self) {
247        self.0.abort();
248    }
249}
250
251/// Path canónico para un flow channel: `$XDG_RUNTIME_DIR/shipote-flow-<id>.sock`.
252pub fn default_flow_socket_path(id: &str) -> PathBuf {
253    let base = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| {
254        let uid = nix::unistd::getuid().as_raw();
255        let p = format!("/run/user/{uid}");
256        if std::path::Path::new(&p).exists() {
257            p
258        } else {
259            "/tmp".into()
260        }
261    });
262    PathBuf::from(base).join(format!("shipote-flow-{id}.sock"))
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268    use tokio::io::AsyncReadExt;
269    use tokio::net::UnixStream;
270
271    #[tokio::test]
272    async fn channel_delivers_to_subscriber() {
273        let tmp = tempfile::tempdir().unwrap();
274        let path = tmp.path().join("flow.sock");
275        let ch = FlowChannel::new(path.clone()).unwrap();
276
277        // Subscriber se conecta.
278        let path_clone = path.clone();
279        let task = tokio::spawn(async move {
280            let mut stream = UnixStream::connect(&path_clone).await.unwrap();
281            let mut buf = vec![0u8; 64];
282            let n = stream.read(&mut buf).await.unwrap();
283            buf.truncate(n);
284            buf
285        });
286
287        // Damos tiempo al accept.
288        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
289        // Hasta que haya 1 receiver_count, el send no llega.
290        for _ in 0..50 {
291            if ch.subscriber_count() >= 1 {
292                break;
293            }
294            tokio::time::sleep(std::time::Duration::from_millis(20)).await;
295        }
296        ch.send(b"hello-flow".to_vec());
297
298        let received = tokio::time::timeout(std::time::Duration::from_secs(2), task)
299            .await
300            .expect("timeout")
301            .unwrap();
302        assert_eq!(received, b"hello-flow");
303    }
304
305    #[tokio::test]
306    async fn replay_buffer_serves_late_subscriber() {
307        let tmp = tempfile::tempdir().unwrap();
308        let path = tmp.path().join("flow.sock");
309        let ch = FlowChannel::new(path.clone()).unwrap();
310
311        // Pushes ANTES de cualquier subscriber: van solo al replay.
312        ch.send(b"chunk-1".to_vec());
313        ch.send(b"chunk-2".to_vec());
314        ch.send(b"chunk-3".to_vec());
315
316        // Subscriber LATE — debe recibir los 3 chunks del replay.
317        let path_clone = path.clone();
318        let task = tokio::spawn(async move {
319            let mut stream = UnixStream::connect(&path_clone).await.unwrap();
320            let mut buf = vec![0u8; 256];
321            // Leemos hasta recibir los 3 chunks (21 bytes esperados).
322            let mut total = Vec::new();
323            for _ in 0..20 {
324                let n = stream.read(&mut buf).await.unwrap();
325                if n == 0 {
326                    break;
327                }
328                total.extend_from_slice(&buf[..n]);
329                if total.len() >= 21 {
330                    break;
331                }
332            }
333            total
334        });
335
336        let received = tokio::time::timeout(std::time::Duration::from_secs(2), task)
337            .await
338            .expect("timeout")
339            .unwrap();
340        let s = String::from_utf8_lossy(&received);
341        assert!(s.contains("chunk-1"), "got: {s:?}");
342        assert!(s.contains("chunk-2"), "got: {s:?}");
343        assert!(s.contains("chunk-3"), "got: {s:?}");
344    }
345
346    #[tokio::test]
347    async fn replay_evicts_by_bytes_cap() {
348        let tmp = tempfile::tempdir().unwrap();
349        let path = tmp.path().join("flow.sock");
350        // chunks=100 (no limita), bytes=20: deberíamos retener sólo los
351        // últimos chunks cuyos bytes sumen ≤ 20.
352        let ch = FlowChannel::with_replay_caps(path.clone(), ReplayCaps::new(100, 20)).unwrap();
353        ch.send(b"AAAAAAAA".to_vec()); // 8 bytes
354        ch.send(b"BBBBBBBB".to_vec()); // 8 → total 16
355        ch.send(b"CCCCCCCC".to_vec()); // 8 → total 24 > 20, evict A → 16
356        ch.send(b"DDDDDDDD".to_vec()); // 8 → total 24 > 20, evict B → 16
357
358        let path_clone = path.clone();
359        let task = tokio::spawn(async move {
360            let mut stream = UnixStream::connect(&path_clone).await.unwrap();
361            let mut buf = vec![0u8; 64];
362            let mut total = Vec::new();
363            for _ in 0..20 {
364                let n = stream.read(&mut buf).await.unwrap();
365                if n == 0 {
366                    break;
367                }
368                total.extend_from_slice(&buf[..n]);
369                if total.len() >= 16 {
370                    break;
371                }
372            }
373            total
374        });
375        let got = tokio::time::timeout(std::time::Duration::from_secs(2), task)
376            .await
377            .expect("timeout")
378            .unwrap();
379        let s = String::from_utf8_lossy(&got);
380        // Sólo C y D (los más viejos A y B fueron evicted).
381        assert!(!s.contains("AAAA"), "should have evicted A: {s:?}");
382        assert!(!s.contains("BBBB"), "should have evicted B: {s:?}");
383        assert!(s.contains("CCCC"), "should keep C: {s:?}");
384        assert!(s.contains("DDDD"), "should keep D: {s:?}");
385    }
386
387    #[tokio::test]
388    async fn drop_removes_socket() {
389        let tmp = tempfile::tempdir().unwrap();
390        let path = tmp.path().join("flow.sock");
391        {
392            let _ch = FlowChannel::new(path.clone()).unwrap();
393            assert!(path.exists());
394        }
395        // Después del drop, el socket file no debe quedar.
396        // Damos un pelín de tiempo al runtime para que el drop corra
397        // mientras estamos en task.
398        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
399        assert!(!path.exists());
400    }
401}