From d7b4886164df5ef048a7c8c0d735fb83555c7990 Mon Sep 17 00:00:00 2001 From: Sergio Date: Fri, 8 May 2026 19:47:21 +0000 Subject: [PATCH] =?UTF-8?q?feat(sidecar):=20Phase=20B-3=20=E2=80=94=20Side?= =?UTF-8?q?carPool=20consolida=20sidecars=20en=20un=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Antes: cada spawn(card) creaba un thread + tokio runtime propio. Para módulos con muchas sesiones (nouser daemon con 50+ Mónadas) eso es 50 threads + 50 runtimes. Ahora: un thread + un runtime tokio current_thread que hostea N tasks de sidecar. API nueva (aditiva, no rompe spawn/spawn_with_handle): let pool = SidecarPool::new()?; pool.spawn(card1); pool.spawn(card2); pool.spawn_conscious(card_with_wit, wit); pool.spawn_with_config(custom_config); // pool drop = todas las sesiones cierran. run_client se hace pública para que el pool pueda enqueuar tasks externos al runtime con handle.spawn(run_client(config)). nouser daemon migrado al pool. Verificación con ps -L: $ ps -L -p $(pidof nouser) LWP CMD 28817 nouser # main thread 28819 brahman-sidecar # pool thread (todas las sesiones) Antes serían 6+ LWP (1 main + N sesiones). Ahora 2 fijos sin importar cuántas Mónadas se publiquen. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 28 +++++++ crates/modules/nouser/core/src/bin/nouser.rs | 32 ++++---- crates/shared/brahman-sidecar/src/lib.rs | 84 +++++++++++++++++++- 3 files changed, 126 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e9c4dd..edc6c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ ratio/diff ver `git show `. ## 2026-05-08 +### feat(sidecar): Phase B-3 — SidecarPool consolida en un runtime +Antes: cada `spawn(card)` creaba un thread + tokio runtime propio. +Para módulos que publican muchas sesiones (nouser daemon con 50+ +Mónadas) eso es 50 threads + 50 runtimes. Ahora: **un thread + un +runtime tokio current_thread** que hostea N tasks de sidecar. + +API nueva (aditiva, no rompe `spawn`/`spawn_with_handle`): + + let pool = SidecarPool::new()?; + pool.spawn(card1); + pool.spawn(card2); + pool.spawn_conscious(card_wit, wit); + pool.spawn_with_config(SidecarConfig::new(c).with_wit(w)); + // pool drop = todas las sesiones cierran. + +`run_client` se hace pública para que el pool pueda enqueuar tasks +externos al runtime con `handle.spawn(run_client(config))`. + +`nouser daemon` migrado al pool. Verificación con `ps -L`: + + $ ps -L -p $(pidof nouser) + LWP CMD + 28817 nouser # main thread + 28819 brahman-sidecar # pool thread (todas las sesiones) + +Antes serían 6+ LWP (1 main + N sesiones); ahora 2 fijos sin importar +cuántas Mónadas se publiquen. + ### feat: Crossreferencia — Card.references como grafo del fractal Las Cards ahora declaran sus relaciones con otras Cards. El Engine posee Mónadas; las Mónadas declaran que son poseídas por el Engine. diff --git a/crates/modules/nouser/core/src/bin/nouser.rs b/crates/modules/nouser/core/src/bin/nouser.rs index f61d241..7e648a2 100644 --- a/crates/modules/nouser/core/src/bin/nouser.rs +++ b/crates/modules/nouser/core/src/bin/nouser.rs @@ -164,6 +164,11 @@ fn cmd_json(args: &[String]) -> Cmd { fn cmd_daemon(args: &[String]) -> Cmd { let dir = require_dir(args)?; + // Pool consolidado: 1 thread + 1 tokio runtime para TODAS las + // sesiones (engine + N mónadas). Antes era 1 thread por sesión. + let pool = brahman_sidecar::SidecarPool::new() + .map_err(|e| format!("crear pool: {e}"))?; + // 1. El propio engine se presenta como Ente. let engine_card = build_engine_card(); let engine_id = engine_card.id; @@ -172,7 +177,7 @@ fn cmd_daemon(args: &[String]) -> Cmd { "nouser daemon: publicando engine '{}' (kind=Ente, id={})", engine_label, engine_id ); - brahman_sidecar::spawn(engine_card); + pool.spawn(engine_card); // 2. Scan y cluster. let (db, n_files) = run_scan(&dir)?; @@ -184,10 +189,9 @@ fn cmd_daemon(args: &[String]) -> Cmd { ); // 3. Cada Mónada se presenta como Card de tipo Data, declarando - // su relación OwnedBy con el engine. La UI puede entonces - // cruzar referencias para reconstruir el grafo - // "nouser_engine posee Mónada X" sin lookup adicional. - let mut handles = Vec::with_capacity(db.monad_count()); + // su relación OwnedBy con el engine. Todas comparten el runtime + // del pool — sin overhead de N threads. + let mut count = 0usize; for monad in db.monads() { let mut card = monad.to_brahman_card(); card.references.push(brahman_card::CardReference { @@ -195,23 +199,17 @@ fn cmd_daemon(args: &[String]) -> Cmd { target_id: engine_id, target_label: engine_label.clone(), }); - match brahman_sidecar::spawn_with_handle(brahman_sidecar::SidecarConfig::new(card)) { - Ok(h) => handles.push(h), - Err(e) => eprintln!( - "nouser daemon: falló sidecar para mónada '{}': {e}", - monad.label - ), - } + pool.spawn(card); + count += 1; } eprintln!( - "nouser daemon: {} sidecars activos (1 ente + {} data). Ctrl-C para terminar.", - handles.len() + 1, - handles.len() + "nouser daemon: 1 ente + {} mónadas en pool consolidado. Ctrl-C para terminar.", + count ); - // 4. Park: el proceso principal queda dormido, los sidecars siguen - // pingueando en sus threads. + // 4. Park: el thread del pool sigue vivo mientras `pool` exista. std::thread::park(); + drop(pool); Ok(()) } diff --git a/crates/shared/brahman-sidecar/src/lib.rs b/crates/shared/brahman-sidecar/src/lib.rs index 4a8469e..d6f3763 100644 --- a/crates/shared/brahman-sidecar/src/lib.rs +++ b/crates/shared/brahman-sidecar/src/lib.rs @@ -16,6 +16,7 @@ #![forbid(unsafe_code)] #![warn(rust_2018_idioms)] +use std::sync::mpsc; use std::thread::JoinHandle; use std::time::Duration; @@ -79,6 +80,85 @@ pub fn spawn_with_handle(config: SidecarConfig) -> std::io::Result, +} + +impl SidecarPool { + /// Crea un pool nuevo. Bloquea hasta que el runtime esté listo. + pub fn new() -> std::io::Result { + let (handle_tx, handle_rx) = mpsc::sync_channel::(0); + let thread = std::thread::Builder::new() + .name("brahman-sidecar-pool".into()) + .spawn(move || { + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + { + Ok(rt) => rt, + Err(e) => { + warn!(error = %e, "tokio runtime falló — pool muerto"); + return; + } + }; + if handle_tx.send(rt.handle().clone()).is_err() { + return; + } + // Mantenemos el runtime vivo mientras existan tasks. + rt.block_on(std::future::pending::<()>()); + })?; + let handle = handle_rx + .recv() + .map_err(|_| std::io::Error::other("pool runtime no respondió"))?; + Ok(Self { + handle, + _thread: thread, + }) + } + + /// Añade una sesión agnóstica al pool (sin WIT). + pub fn spawn(&self, card: Card) { + self.spawn_with_config(SidecarConfig::new(card)); + } + + /// Añade una sesión consciente (con WIT) al pool. + pub fn spawn_conscious(&self, card: Card, wit: WitInterface) { + self.spawn_with_config(SidecarConfig::new(card).with_wit(wit)); + } + + /// Añade una sesión con configuración custom. + pub fn spawn_with_config(&self, config: SidecarConfig) { + self.handle.spawn(run_client(config)); + } +} + +impl Default for SidecarPool { + fn default() -> Self { + Self::new().expect("falló SidecarPool::new") + } +} + fn run_thread(config: SidecarConfig) { let rt = match tokio::runtime::Builder::new_current_thread() .enable_io() @@ -94,7 +174,9 @@ fn run_thread(config: SidecarConfig) { rt.block_on(run_client(config)); } -async fn run_client(config: SidecarConfig) { +/// Bucle async del sidecar. Público para que `SidecarPool` lo use vía +/// `handle.spawn(run_client(...))` desde código externo al runtime. +pub async fn run_client(config: SidecarConfig) { let path = transport::default_socket_path(); let conscious = config.wit.is_some(); let mut client = match Client::connect_with(&path, config.card, config.wit).await {