Files
brahman/crates/core/brahman-admin/src/server.rs
T
Sergio bbb9a9d2f5 feat(broker): priority contexts — biases per-contexto operativo
Cierra el último pendiente de feature: el broker ahora puede operar
bajo un contexto (test/prod/foreground/secure/etc) que activa biases
declarados en las Cards.

Schema (brahman-card):
- ContextBias { pin_to: Option<String>, priority_offset: i8 }.
- Card.priority_contexts: BTreeMap<String, ContextBias>, también en
  WireCard. Las conversiones From propagan el campo.

Comportamiento (brahman-broker):
- BrokerConfig.current_context: Option<String>. Cuando es Some(ctx) y
  una Card tiene priority_contexts.get(ctx), el bias aplica:
   - Consumer-side: bias.pin_to sobreescribe Flow.pin_to estático.
   - Producer-side: bias.priority_offset se suma a la priority base
     (clamp en [Low=0, Critical=3]).
- BrokeredCard propaga priority_contexts. find_producer_for usa
  effective_priority y context_bias en lugar de comparar Priority
  directo.

Observabilidad:
- AdminConfig.current_context + StatusSnapshot.current_context.
- brahman-status imprime "Context: <nombre>" si está activo.

Wiring:
- ente-zero lee BRAHMAN_BROKER_CONTEXT del entorno y la propaga al
  broker y al admin. Sin var, biases inactivos (back-compat total).

Tests nuevos (brahman-broker, +4):
- context_priority_offset_lifts_producer_above_alphabetic_winner:
  sin contexto a-prod gana por alfabético; con context "test" b-prod
  gana por offset +1.
- context_pin_to_overrides_static_pin: static pin "real-dht", test
  override "mock-dht" → mock gana en context "test".
- unknown_context_no_op: biases declarados para "test" no aplican
  cuando broker está en "prod".
- priority_offset_clamps_to_critical: offset enorme se clampa a 3.

Validación end-to-end manual:
  $ BRAHMAN_BROKER_CONTEXT=test ente-zero &
  $ brahman-status
  Init: server=0.1.0 protocol=0.1.0 attached=true
  Context: test

Tests acumulados: 39 (card 11, broker 15, handshake codec+transport 2 +
integ 7, card-wit 4, admin 0). cargo check --workspace: 0 errores, 0
warnings.

Con esto cierran TODOS los pendientes técnicos abiertos. El único
"pendiente" que queda es el caso real para extender (priority
contexts per-deployment, scheduling biases dinámicos, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:46:59 +00:00

111 lines
3.3 KiB
Rust

//! Servidor admin: emite un `StatusSnapshot` JSON por conexión y cierra.
use std::path::{Path, PathBuf};
use std::sync::Arc;
use brahman_broker::Broker;
use tokio::io::AsyncWriteExt;
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::Mutex;
use tracing::warn;
use crate::snapshot::StatusSnapshot;
/// Configuración del servidor admin.
#[derive(Debug, Clone, Default)]
pub struct AdminConfig {
/// `true` si el Init está atado al servidor que aloja este admin.
pub init_attached: bool,
/// Contexto operativo del broker, espejado en el snapshot.
pub current_context: Option<String>,
}
/// Servidor admin escuchando en un Unix socket.
pub struct AdminServer {
listener: UnixListener,
socket_path: PathBuf,
broker: Arc<Mutex<Broker>>,
config: AdminConfig,
}
impl AdminServer {
/// Crea el listener. Si `path` existe, lo elimina (asume socket stale).
pub fn bind(
path: impl Into<PathBuf>,
broker: Arc<Mutex<Broker>>,
config: AdminConfig,
) -> std::io::Result<Self> {
let socket_path = path.into();
if socket_path.exists() {
std::fs::remove_file(&socket_path)?;
}
if let Some(parent) = socket_path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
let listener = UnixListener::bind(&socket_path)?;
Ok(Self {
listener,
socket_path,
broker,
config,
})
}
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
/// Loop de aceptación: cada conexión recibe un snapshot y se cierra.
pub async fn run(self) -> std::io::Result<()> {
loop {
let (stream, _addr) = self.listener.accept().await?;
let broker = self.broker.clone();
let config = self.config.clone();
tokio::spawn(async move {
if let Err(e) = handle_conn(stream, broker, config).await {
warn!(error = %e, "admin conn falló");
}
});
}
}
}
impl Drop for AdminServer {
fn drop(&mut self) {
if let Err(e) = std::fs::remove_file(&self.socket_path) {
if e.kind() != std::io::ErrorKind::NotFound {
warn!(path = %self.socket_path.display(), error = %e, "no se pudo limpiar admin socket");
}
}
}
}
async fn handle_conn(
mut stream: UnixStream,
broker: Arc<Mutex<Broker>>,
config: AdminConfig,
) -> std::io::Result<()> {
let snapshot = build_snapshot(&broker, &config).await;
let mut json = serde_json::to_string(&snapshot)?;
json.push('\n');
stream.write_all(json.as_bytes()).await?;
stream.shutdown().await?;
Ok(())
}
async fn build_snapshot(broker: &Arc<Mutex<Broker>>, config: &AdminConfig) -> StatusSnapshot {
let b = broker.lock().await;
let sessions: Vec<_> = b.cards().cloned().collect();
let matches = b.all_matches();
StatusSnapshot {
server_version: crate::ADMIN_VERSION.to_string(),
protocol_version: brahman_card::PROTOCOL_VERSION.to_string(),
init_attached: config.init_attached,
current_context: config.current_context.clone(),
sessions,
matches,
}
}