refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "brahman-sidecar"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "Brahman — sidecar reusable: thread + tokio runtime que mantiene viva la sesión de un módulo contra el Init."
|
||||
|
||||
[dependencies]
|
||||
brahman-card = { path = "../brahman-card" }
|
||||
brahman-handshake = { path = "../brahman-handshake" }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tracing-subscriber = { workspace = true }
|
||||
brahman-card-wit = { path = "../brahman-card-wit" }
|
||||
|
||||
[[example]]
|
||||
name = "presence"
|
||||
path = "examples/presence.rs"
|
||||
|
||||
[[example]]
|
||||
name = "presence-conscious"
|
||||
path = "examples/presence-conscious.rs"
|
||||
@@ -0,0 +1,99 @@
|
||||
//! `presence-conscious` — módulo brahman que se presenta con su WIT.
|
||||
//!
|
||||
//! Variante de [`presence`] que toma un path a un archivo `.wit` (default
|
||||
//! `shared_wit/protocol.wit` resuelto desde el cwd) y lo parsea con
|
||||
//! `brahman-card-wit` antes de spawnear el sidecar. Demuestra el flujo
|
||||
//! "módulo consciente": Hello incluye `WitInterface`, el server lo
|
||||
//! registra como `ResolvedCard::from_conscious`, y aparece con marker
|
||||
//! 🧠 en `brahman-status`.
|
||||
//!
|
||||
//! Uso:
|
||||
//! ```sh
|
||||
//! cargo run -p brahman-sidecar --example presence-conscious -- mi-modulo [path/al.wit]
|
||||
//! ```
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_card::{
|
||||
ulid::Ulid, Card, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef,
|
||||
CARD_SCHEMA_VERSION,
|
||||
};
|
||||
use brahman_sidecar::{spawn_with_handle, SidecarConfig};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
let label = args.next().unwrap_or_else(|| "conscious-default".into());
|
||||
let wit_path: PathBuf = args
|
||||
.next()
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("shared_wit/protocol.wit"));
|
||||
|
||||
let wit = match brahman_card_wit::parse_wit_file(&wit_path) {
|
||||
Ok(worlds) => match worlds.into_iter().next() {
|
||||
Some(w) => {
|
||||
eprintln!(
|
||||
"[{label}] cargado wit: {} / {}",
|
||||
w.package, w.world
|
||||
);
|
||||
Some(w)
|
||||
}
|
||||
None => {
|
||||
eprintln!("[{label}] {} no declara worlds", wit_path.display());
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("[{label}] falló parse de {}: {e}", wit_path.display());
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let card = Card {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
label: label.clone(),
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
lifecycle: Lifecycle::Daemon,
|
||||
priority: Priority::Normal,
|
||||
provides: BTreeSet::new(),
|
||||
requires: BTreeSet::new(),
|
||||
flow: Flows {
|
||||
input: vec![Flow {
|
||||
name: "in".into(),
|
||||
ty: TypeRef::Primitive {
|
||||
name: "json".into(),
|
||||
},
|
||||
pin_to: None,
|
||||
}],
|
||||
output: vec![Flow {
|
||||
name: "out".into(),
|
||||
ty: TypeRef::Primitive {
|
||||
name: "json".into(),
|
||||
},
|
||||
pin_to: None,
|
||||
}],
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let config = SidecarConfig {
|
||||
card,
|
||||
wit,
|
||||
ping_interval: Duration::from_secs(5),
|
||||
};
|
||||
|
||||
let _handle = spawn_with_handle(config);
|
||||
|
||||
eprintln!("[{label}] sidecar lanzado, durmiendo (Ctrl-C para salir)");
|
||||
std::thread::park();
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//! `presence` — módulo brahman dummy para pruebas y demos.
|
||||
//!
|
||||
//! Declara una Card mínima con label tomado del primer argumento (default
|
||||
//! `presence-default`) y mantiene la sesión viva hasta SIGTERM/SIGINT.
|
||||
//! Útil para poblar el broker con sesiones de prueba.
|
||||
//!
|
||||
//! Uso:
|
||||
//! ```sh
|
||||
//! cargo run -p brahman-sidecar --example presence -- mi-modulo
|
||||
//! ```
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_card::{
|
||||
ulid::Ulid, Card, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef,
|
||||
CARD_SCHEMA_VERSION,
|
||||
};
|
||||
use brahman_sidecar::{spawn_with_handle, SidecarConfig};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let label = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "presence-default".into());
|
||||
|
||||
let card = Card {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
label: label.clone(),
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
lifecycle: Lifecycle::Daemon,
|
||||
priority: Priority::Normal,
|
||||
provides: BTreeSet::new(),
|
||||
requires: BTreeSet::new(),
|
||||
flow: Flows {
|
||||
input: vec![Flow {
|
||||
name: "in".into(),
|
||||
ty: TypeRef::Primitive {
|
||||
name: "json".into(),
|
||||
},
|
||||
pin_to: None,
|
||||
}],
|
||||
output: vec![Flow {
|
||||
name: "out".into(),
|
||||
ty: TypeRef::Primitive {
|
||||
name: "json".into(),
|
||||
},
|
||||
pin_to: None,
|
||||
}],
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _handle = spawn_with_handle(SidecarConfig {
|
||||
card,
|
||||
wit: None,
|
||||
ping_interval: Duration::from_secs(5),
|
||||
});
|
||||
|
||||
eprintln!("presence({label}): sidecar lanzado, durmiendo (Ctrl-C para salir)");
|
||||
std::thread::park();
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
//! `brahman-sidecar::discovery` — API reusable para que un módulo
|
||||
//! consumer encuentre proveedores vivos vía broker, sin hardcodear
|
||||
//! sockets ni reimplementar el patrón a mano.
|
||||
//!
|
||||
//! Es la generalización de `discover_producer_socket` del CLI
|
||||
//! `akasha attract --remote`: declarás el `TypeRef` que querés
|
||||
//! consumir y el broker te empuja un `MatchEvent::Available` con el
|
||||
//! `producer_service_socket` del primer proveedor matched.
|
||||
//!
|
||||
//! Pipeline:
|
||||
//! 1. `build_consumer_card(label, flow_name, type_name)` arma una
|
||||
//! Card mínima (Ente, Oneshot, Virtual) con un input flow.
|
||||
//! 2. `await_provider(card, timeout)` se conecta al brahman-init,
|
||||
//! espera hasta `timeout` por `MatchEvent::Available`, devuelve
|
||||
//! el socket del proveedor electo, y envía Farewell.
|
||||
//! 3. Para mundos blocking (CLIs, tests, std-thread loops) hay
|
||||
//! `await_provider_blocking` que arma su propio runtime
|
||||
//! `current_thread`.
|
||||
//!
|
||||
//! Quién elige al proveedor es el broker, no este módulo. Si el
|
||||
//! broker tiene `priority_contexts` activo, podés cambiar de
|
||||
//! proveedor sin tocar el consumer; el matching dinámico se respeta.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use brahman_card::{
|
||||
Card, CardKind, Flow, Flows, Lifecycle, Payload, Priority, Supervision, TypeRef,
|
||||
};
|
||||
use brahman_handshake::client::{Client, ClientError};
|
||||
use brahman_handshake::messages::MatchEventKind;
|
||||
use brahman_handshake::transport;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ConsumerError {
|
||||
#[error("no se pudo conectar al init en {socket}: {source}")]
|
||||
Connect {
|
||||
socket: PathBuf,
|
||||
#[source]
|
||||
source: ClientError,
|
||||
},
|
||||
#[error("error en cliente brahman: {0}")]
|
||||
Client(#[from] ClientError),
|
||||
#[error("timeout {timeout:?} sin proveedor disponible para flow '{flow}' (type '{type_ref}')")]
|
||||
NoProvider {
|
||||
flow: String,
|
||||
type_ref: String,
|
||||
timeout: Duration,
|
||||
},
|
||||
#[error("no se pudo crear runtime tokio: {0}")]
|
||||
Runtime(String),
|
||||
}
|
||||
|
||||
/// Construye una Card mínima de consumer que declara un input flow
|
||||
/// con el `TypeRef::Primitive { name }` solicitado. Usá esto para
|
||||
/// el caso común; si necesitás algo más rico (output flows,
|
||||
/// permissions, references) construí la Card a mano y pasala a
|
||||
/// [`await_provider`] directamente.
|
||||
pub fn build_consumer_card(
|
||||
consumer_label: impl Into<String>,
|
||||
flow_name: impl Into<String>,
|
||||
type_name: impl Into<String>,
|
||||
) -> Card {
|
||||
Card {
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
lifecycle: Lifecycle::Oneshot,
|
||||
priority: Priority::Normal,
|
||||
kind: CardKind::Ente,
|
||||
flow: Flows {
|
||||
input: vec![Flow {
|
||||
name: flow_name.into(),
|
||||
ty: TypeRef::Primitive {
|
||||
name: type_name.into(),
|
||||
},
|
||||
pin_to: None,
|
||||
}],
|
||||
output: vec![],
|
||||
},
|
||||
..Card::new(consumer_label)
|
||||
}
|
||||
}
|
||||
|
||||
/// Conecta al brahman-init, registra `consumer_card`, espera el
|
||||
/// primer `MatchEvent::Available` y devuelve el `producer_service_socket`
|
||||
/// que el broker emitió. Cierra la sesión con Farewell antes de
|
||||
/// retornar (best-effort).
|
||||
///
|
||||
/// La `consumer_card` debe declarar al menos un `flow.input`; si no,
|
||||
/// el broker no puede hacer matching y el await siempre dará timeout.
|
||||
pub async fn await_provider(
|
||||
consumer_card: Card,
|
||||
timeout: Duration,
|
||||
) -> Result<PathBuf, ConsumerError> {
|
||||
let init_path = transport::default_socket_path();
|
||||
|
||||
// Capturamos descriptor para el mensaje de error antes de mover
|
||||
// la card al cliente.
|
||||
let (flow_name, type_ref_name) = describe_first_input(&consumer_card);
|
||||
|
||||
let mut client = Client::connect(&init_path, consumer_card)
|
||||
.await
|
||||
.map_err(|source| ConsumerError::Connect {
|
||||
socket: init_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let deadline = Instant::now() + timeout;
|
||||
let socket = loop {
|
||||
let remaining = deadline.saturating_duration_since(Instant::now());
|
||||
if remaining.is_zero() {
|
||||
break None;
|
||||
}
|
||||
match client.await_event(remaining).await? {
|
||||
Some(ev) if ev.kind == MatchEventKind::Available => {
|
||||
break ev.producer_service_socket;
|
||||
}
|
||||
Some(_) => continue, // Lost u otros: seguir esperando hasta el deadline
|
||||
None => break None,
|
||||
}
|
||||
};
|
||||
|
||||
let _ = client.farewell().await; // best-effort cleanup
|
||||
|
||||
socket.ok_or(ConsumerError::NoProvider {
|
||||
flow: flow_name,
|
||||
type_ref: type_ref_name,
|
||||
timeout,
|
||||
})
|
||||
}
|
||||
|
||||
/// Wrapper bloqueante de [`await_provider`]. Crea un runtime tokio
|
||||
/// `current_thread` efímero y bloquea el thread llamador. Útil para
|
||||
/// CLIs, tests y módulos std-thread (p. ej. el frontend GPUI antes
|
||||
/// de tener su propio runtime async).
|
||||
pub fn await_provider_blocking(
|
||||
consumer_card: Card,
|
||||
timeout: Duration,
|
||||
) -> Result<PathBuf, ConsumerError> {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
.map_err(|e| ConsumerError::Runtime(e.to_string()))?;
|
||||
|
||||
rt.block_on(await_provider(consumer_card, timeout))
|
||||
}
|
||||
|
||||
/// Conecta al brahman-init con una Card observer (sin inputs ni
|
||||
/// outputs) y pide la lista de sesiones activas. Útil para
|
||||
/// herramientas de observabilidad (broker-explorer, CLIs).
|
||||
///
|
||||
/// El observer se identifica con `observer_label`. La sesión se
|
||||
/// cierra con Farewell antes de retornar (best-effort).
|
||||
pub async fn list_sessions(
|
||||
observer_label: impl Into<String>,
|
||||
) -> Result<brahman_handshake::messages::SessionList, ConsumerError> {
|
||||
let init_path = transport::default_socket_path();
|
||||
// Card mínima sin flow.input/output: el observer no participa en
|
||||
// matching, sólo establece sesión para poder consultar.
|
||||
let card = Card {
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
lifecycle: Lifecycle::Oneshot,
|
||||
priority: Priority::Normal,
|
||||
kind: CardKind::Ente,
|
||||
flow: Flows {
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
},
|
||||
..Card::new(observer_label)
|
||||
};
|
||||
|
||||
let mut client = Client::connect(&init_path, card)
|
||||
.await
|
||||
.map_err(|source| ConsumerError::Connect {
|
||||
socket: init_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let list = client.list_sessions().await?;
|
||||
let _ = client.farewell().await;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Wrapper bloqueante de [`list_sessions`]. Idéntico patrón a
|
||||
/// `await_provider_blocking`: runtime current_thread efímero.
|
||||
pub fn list_sessions_blocking(
|
||||
observer_label: impl Into<String>,
|
||||
) -> Result<brahman_handshake::messages::SessionList, ConsumerError> {
|
||||
let label = observer_label.into();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
.map_err(|e| ConsumerError::Runtime(e.to_string()))?;
|
||||
rt.block_on(list_sessions(label))
|
||||
}
|
||||
|
||||
/// Análogo a `list_sessions` pero pide los matches activos del
|
||||
/// broker. La Card observer es la misma forma minimalista (sin
|
||||
/// flow.input/output) — el endpoint no requiere participar en
|
||||
/// matching.
|
||||
pub async fn list_matches(
|
||||
observer_label: impl Into<String>,
|
||||
) -> Result<brahman_handshake::messages::MatchList, ConsumerError> {
|
||||
let init_path = transport::default_socket_path();
|
||||
let card = Card {
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
lifecycle: Lifecycle::Oneshot,
|
||||
priority: Priority::Normal,
|
||||
kind: CardKind::Ente,
|
||||
flow: Flows {
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
},
|
||||
..Card::new(observer_label)
|
||||
};
|
||||
|
||||
let mut client = Client::connect(&init_path, card)
|
||||
.await
|
||||
.map_err(|source| ConsumerError::Connect {
|
||||
socket: init_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
let list = client.list_matches().await?;
|
||||
let _ = client.farewell().await;
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
/// Wrapper bloqueante de [`list_matches`].
|
||||
pub fn list_matches_blocking(
|
||||
observer_label: impl Into<String>,
|
||||
) -> Result<brahman_handshake::messages::MatchList, ConsumerError> {
|
||||
let label = observer_label.into();
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
.map_err(|e| ConsumerError::Runtime(e.to_string()))?;
|
||||
rt.block_on(list_matches(label))
|
||||
}
|
||||
|
||||
fn describe_first_input(card: &Card) -> (String, String) {
|
||||
match card.flow.input.first() {
|
||||
Some(flow) => {
|
||||
let type_name = match &flow.ty {
|
||||
TypeRef::Primitive { name } => name.clone(),
|
||||
TypeRef::Wit { package, name, .. } => format!("{package}#{name}"),
|
||||
};
|
||||
(flow.name.clone(), type_name)
|
||||
}
|
||||
None => ("(sin input)".into(), "(sin tipo)".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use brahman_card::ulid::Ulid;
|
||||
|
||||
#[test]
|
||||
fn builder_sets_input_flow_with_primitive_type() {
|
||||
let c = build_consumer_card("akasha.cli", "embed-result", "json");
|
||||
assert_eq!(c.label, "akasha.cli");
|
||||
assert_eq!(c.kind, CardKind::Ente);
|
||||
assert!(matches!(c.lifecycle, Lifecycle::Oneshot));
|
||||
assert!(matches!(c.supervision, Supervision::OneShot));
|
||||
assert_eq!(c.flow.input.len(), 1);
|
||||
let f = &c.flow.input[0];
|
||||
assert_eq!(f.name, "embed-result");
|
||||
match &f.ty {
|
||||
TypeRef::Primitive { name } => assert_eq!(name, "json"),
|
||||
_ => panic!("expected primitive type"),
|
||||
}
|
||||
assert!(c.flow.output.is_empty());
|
||||
// El builder asigna un id real (no nil) — fundamental para que
|
||||
// el broker no colisione con otros consumers.
|
||||
assert!(c.id != Ulid::nil(), "consumer card id no debe ser nil");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builder_assigns_distinct_ids_per_call() {
|
||||
let a = build_consumer_card("a", "f", "t");
|
||||
let b = build_consumer_card("a", "f", "t");
|
||||
assert_ne!(a.id, b.id, "cada Card debería tener id propio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_falls_back_when_no_input_flow() {
|
||||
let mut c = build_consumer_card("x", "f", "t");
|
||||
c.flow.input.clear();
|
||||
let (flow, ty) = describe_first_input(&c);
|
||||
assert_eq!(flow, "(sin input)");
|
||||
assert_eq!(ty, "(sin tipo)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_formats_wit_type() {
|
||||
let mut c = build_consumer_card("x", "f", "t");
|
||||
c.flow.input[0].ty = TypeRef::Wit {
|
||||
package: "brahman:dht".into(),
|
||||
interface: None,
|
||||
name: "entity-result".into(),
|
||||
};
|
||||
let (_, ty) = describe_first_input(&c);
|
||||
assert_eq!(ty, "brahman:dht#entity-result");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
//! `brahman-sidecar` — boilerplate del cliente brahman extraído.
|
||||
//!
|
||||
//! Cualquier módulo que quiera presentarse al Init brahman pero que tenga
|
||||
//! su propio runtime (GPUI, current_thread tokio, std-thread loop, etc.)
|
||||
//! puede llamar [`spawn`] con su [`brahman_card::Card`]. Eso arma un
|
||||
//! thread aparte con un runtime tokio current_thread, conecta al Init,
|
||||
//! y mantiene la sesión viva con pings periódicos.
|
||||
//!
|
||||
//! Si el Init no está disponible, el thread loggea y termina — el módulo
|
||||
//! sigue funcionando standalone.
|
||||
//!
|
||||
//! Errores de conexión / ping se loggean vía `tracing::warn!`. Si querés
|
||||
//! capturar la salida del thread (por ejemplo para test), usá
|
||||
//! [`spawn_with_handle`] que devuelve un `JoinHandle`.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
pub mod discovery;
|
||||
pub use discovery::{
|
||||
await_provider, await_provider_blocking, build_consumer_card, list_matches,
|
||||
list_matches_blocking, list_sessions, list_sessions_blocking, ConsumerError,
|
||||
};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_card::{ulid::Ulid, Card, WitInterface};
|
||||
use brahman_handshake::{client::Client, transport};
|
||||
use tokio::task::AbortHandle;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Período entre pings al Init.
|
||||
pub const DEFAULT_PING_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Configuración del sidecar.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SidecarConfig {
|
||||
/// Card que se presenta al Init.
|
||||
pub card: Card,
|
||||
/// WIT interface opcional. Si es `Some`, el módulo se registra como
|
||||
/// "consciente" (`ResolvedCard::from_conscious`).
|
||||
pub wit: Option<WitInterface>,
|
||||
/// Período entre pings.
|
||||
pub ping_interval: Duration,
|
||||
}
|
||||
|
||||
impl SidecarConfig {
|
||||
/// Configuración agnóstica con defaults razonables (sin WIT, ping 30s).
|
||||
pub fn new(card: Card) -> Self {
|
||||
Self {
|
||||
card,
|
||||
wit: None,
|
||||
ping_interval: DEFAULT_PING_INTERVAL,
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuración consciente con WIT extraída.
|
||||
pub fn with_wit(mut self, wit: WitInterface) -> Self {
|
||||
self.wit = Some(wit);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn fire-and-forget agnóstico. Para módulos conscientes usá
|
||||
/// [`spawn_conscious`] o [`spawn_with_handle`] con un `SidecarConfig`
|
||||
/// personalizado.
|
||||
pub fn spawn(card: Card) {
|
||||
if let Err(e) = spawn_with_handle(SidecarConfig::new(card)) {
|
||||
warn!(error = %e, "no se pudo spawnear el sidecar brahman");
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn fire-and-forget con WIT. Idéntico a [`spawn`] pero el módulo se
|
||||
/// registra como consciente en el broker.
|
||||
pub fn spawn_conscious(card: Card, wit: WitInterface) {
|
||||
if let Err(e) = spawn_with_handle(SidecarConfig::new(card).with_wit(wit)) {
|
||||
warn!(error = %e, "no se pudo spawnear el sidecar brahman");
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn devolviendo el `JoinHandle` para tests o cleanup explícito.
|
||||
pub fn spawn_with_handle(config: SidecarConfig) -> std::io::Result<JoinHandle<()>> {
|
||||
std::thread::Builder::new()
|
||||
.name("brahman-sidecar".into())
|
||||
.spawn(move || run_thread(config))
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// SidecarPool — un solo runtime tokio compartido por N sesiones
|
||||
// =====================================================================
|
||||
|
||||
/// Pool consolidado: un único thread con un runtime tokio
|
||||
/// `current_thread` que hostea N tasks de sidecar simultáneas.
|
||||
///
|
||||
/// Para módulos con muchas sesiones (p. ej. `akasha daemon` publicando
|
||||
/// 50+ Mónadas), evita el costo de tener un thread+runtime por cada
|
||||
/// sesión.
|
||||
///
|
||||
/// **API**:
|
||||
/// - `SidecarPool::new()` crea el pool (spawn del thread runtime).
|
||||
/// - `pool.spawn(card)` añade una sesión sin WIT.
|
||||
/// - `pool.spawn_conscious(card, wit)` añade una sesión con WIT.
|
||||
/// - `pool.spawn_with_config(config)` para configuración custom.
|
||||
///
|
||||
/// El pool se mantiene vivo mientras exista. Si el `SidecarPool`
|
||||
/// se dropea, el thread interno termina y todas las sesiones cierran.
|
||||
pub struct SidecarPool {
|
||||
handle: tokio::runtime::Handle,
|
||||
/// Sesiones vivas indexadas por `Card.id`. Permite que un nuevo
|
||||
/// `spawn` con el mismo id aborte la sesión previa — útil cuando
|
||||
/// un módulo (p. ej. `akasha daemon`) re-publica una Mónada cuya
|
||||
/// composición cambió.
|
||||
sessions: Arc<Mutex<HashMap<Ulid, AbortHandle>>>,
|
||||
_thread: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl SidecarPool {
|
||||
/// Crea un pool nuevo. Bloquea hasta que el runtime esté listo.
|
||||
pub fn new() -> std::io::Result<Self> {
|
||||
let (handle_tx, handle_rx) = mpsc::sync_channel::<tokio::runtime::Handle>(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,
|
||||
sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
_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.
|
||||
///
|
||||
/// Si ya existía una sesión para el mismo `Card.id`, la previa
|
||||
/// se aborta antes de spawnear la nueva. Esto hace `spawn`
|
||||
/// idempotente respecto al id: re-publicar una Mónada cuya
|
||||
/// composición cambió "refresca" la sesión en el broker.
|
||||
pub fn spawn_with_config(&self, config: SidecarConfig) {
|
||||
let card_id = config.card.id;
|
||||
let join = self.handle.spawn(run_client(config));
|
||||
let abort = join.abort_handle();
|
||||
if let Ok(mut sessions) = self.sessions.lock() {
|
||||
if let Some(prev) = sessions.insert(card_id, abort) {
|
||||
prev.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cierra explícitamente la sesión asociada a `card_id`. No-op si
|
||||
/// no había sesión registrada.
|
||||
pub fn drop_session(&self, card_id: Ulid) {
|
||||
if let Ok(mut sessions) = self.sessions.lock() {
|
||||
if let Some(abort) = sessions.remove(&card_id) {
|
||||
abort.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cantidad actual de sesiones vivas (estimada — puede haber
|
||||
/// drift transitorio entre abort y limpieza).
|
||||
pub fn live_sessions(&self) -> usize {
|
||||
self.sessions.lock().map(|s| s.len()).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
.enable_time()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "tokio runtime falló");
|
||||
return;
|
||||
}
|
||||
};
|
||||
rt.block_on(run_client(config));
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
Ok(c) => {
|
||||
info!(
|
||||
target: "brahman_sidecar",
|
||||
session = %c.session(),
|
||||
init_attached = c.server_info().init_attached,
|
||||
server = %c.server_info().server_version,
|
||||
conscious,
|
||||
"attached"
|
||||
);
|
||||
c
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
target: "brahman_sidecar",
|
||||
error = %e,
|
||||
socket = %path.display(),
|
||||
"no conectado"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(config.ping_interval).await;
|
||||
if let Err(e) = client.ping().await {
|
||||
warn!(target: "brahman_sidecar", error = %e, "ping falló — terminando sidecar");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user