feat(handshake): notificación push de matches del broker al cliente
El servidor empuja MatchEvent (Available | Lost) a los consumers cuando
sus inputs cambian de match — sea porque un productor llegó, porque
otro mejor lo desplazó, o porque desapareció.
Mecánica:
- Frame::MatchEvent con MatchEventKind { Available, Lost } y los datos
del match (consumer_flow, producer_session/label/flow, ty, via, pinned).
- Server: SessionTxTable (Arc<Mutex<HashMap<SessionId, mpsc::Sender>>>)
+ LastMatches (último match conocido por consumer/input). En cada
register/unregister, broadcast_match_diffs recomputa con el broker
y emite SOLO los diffs respecto al estado anterior.
- Session::run_post_handshake usa tokio::select! para multiplexar
read_frame del cliente y rx.recv() de su tx push.
- Cleanup ahora también limpia push_table y last_matches y dispara un
broadcast (para notificar a quienes pierden el match).
- Client: VecDeque<MatchEvent> bufferea eventos que llegan mezclados
con respuestas a Ping. API:
- take_event() — non-blocking, drena buffer
- await_event(timeout) — bloquea hasta evento o timeout
- ping() ahora drena MatchEvents intermedios hasta encontrar el Pong.
Capacity del canal push por sesión: 32 frames (try_send no-blocking;
si se llena, los eventos extra se descartan — se documenta como
ephemeral, el cliente puede re-consultar via brahman-status).
Test nuevo en brahman-handshake/tests/handshake.rs:
- match_event_pushed_on_producer_arrival: consumer espera, no recibe
evento → llega productor → recibe Available → productor se va →
recibe Lost.
Example nuevo: brahman-handshake/examples/subscriber.rs — cliente que
loguea cada MatchEvent en tiempo real. Útil para ver la dinámica del
broker. Pings cada 25s para keepalive.
Demo end-to-end verificada (4 eventos, 3 ya cubren el ciclo completo):
T+0.3 alpha llega → Available ← demo.alpha.out
T+0.8 beta llega → (sin evento: alpha gana por orden alfabético)
T+1.3 alpha killed → Available ← demo.beta.out (re-evaluación)
T+1.8 beta killed → Lost ← <none>
El broker emite diff: ningún evento cuando un nuevo productor llega
sin desplazar al ganador actual.
Tests: 28/28 (handshake integ 6→7). cargo check --workspace: 0 errores.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
//! Cliente de handshake. Conecta a un Unix socket y mantiene la sesión.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_card::{Card, CARD_SCHEMA_VERSION};
|
||||
use thiserror::Error;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
use crate::codec::{read_frame, write_frame};
|
||||
use crate::messages::{Farewell, Frame, HandshakeError, Hello, HelloAck, Ping, SessionId};
|
||||
use crate::messages::{Farewell, Frame, HandshakeError, Hello, HelloAck, MatchEvent, Ping, SessionId};
|
||||
|
||||
/// Errores del cliente.
|
||||
#[derive(Debug, Error)]
|
||||
@@ -29,12 +31,15 @@ pub enum ClientError {
|
||||
}
|
||||
|
||||
/// Cliente conectado y autenticado. Tras `connect` ya completó el handshake
|
||||
/// y tiene su `SessionId`.
|
||||
/// y tiene su `SessionId`. Los `MatchEvent` recibidos durante operaciones
|
||||
/// request/response se buferean en `pending_events` y se obtienen vía
|
||||
/// [`Client::take_event`] o [`Client::await_event`].
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
stream: UnixStream,
|
||||
session: SessionId,
|
||||
server_info: HelloAck,
|
||||
pending_events: VecDeque<MatchEvent>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
@@ -60,11 +65,17 @@ impl Client {
|
||||
Frame::Ping(_) => return Err(ClientError::UnexpectedFrame { got: "Ping" }),
|
||||
Frame::Pong(_) => return Err(ClientError::UnexpectedFrame { got: "Pong" }),
|
||||
Frame::Farewell(_) => return Err(ClientError::UnexpectedFrame { got: "Farewell" }),
|
||||
Frame::MatchEvent(_) => {
|
||||
return Err(ClientError::UnexpectedFrame {
|
||||
got: "MatchEvent (pre-handshake)",
|
||||
});
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
stream,
|
||||
session: ack.session,
|
||||
server_info: ack,
|
||||
pending_events: VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,7 +89,8 @@ impl Client {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
/// Envía un Ping y devuelve el timestamp del servidor.
|
||||
/// Envía un Ping y devuelve el timestamp del servidor. Los frames
|
||||
/// `MatchEvent` que lleguen mezclados se buferean en `pending_events`.
|
||||
pub async fn ping(&mut self) -> Result<u64, ClientError> {
|
||||
write_frame(
|
||||
&mut self.stream,
|
||||
@@ -87,10 +99,39 @@ impl Client {
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
match read_frame(&mut self.stream).await? {
|
||||
Frame::Pong(p) => Ok(p.timestamp_ms),
|
||||
Frame::Error(e) => Err(ClientError::Server(e)),
|
||||
_ => Err(ClientError::UnexpectedFrame { got: "non-pong" }),
|
||||
loop {
|
||||
match read_frame(&mut self.stream).await? {
|
||||
Frame::Pong(p) => return Ok(p.timestamp_ms),
|
||||
Frame::MatchEvent(ev) => self.pending_events.push_back(ev),
|
||||
Frame::Error(e) => return Err(ClientError::Server(e)),
|
||||
_ => return Err(ClientError::UnexpectedFrame { got: "non-pong" }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Saca un evento pendiente del buffer, sin bloquear ni leer del wire.
|
||||
pub fn take_event(&mut self) -> Option<MatchEvent> {
|
||||
self.pending_events.pop_front()
|
||||
}
|
||||
|
||||
/// Espera un `MatchEvent` con timeout. Drena primero el buffer; si
|
||||
/// está vacío, lee del wire hasta el timeout. Otros frames recibidos
|
||||
/// (Pong huérfano, Error) cortan la espera con error.
|
||||
pub async fn await_event(
|
||||
&mut self,
|
||||
timeout: Duration,
|
||||
) -> Result<Option<MatchEvent>, ClientError> {
|
||||
if let Some(ev) = self.pending_events.pop_front() {
|
||||
return Ok(Some(ev));
|
||||
}
|
||||
match tokio::time::timeout(timeout, read_frame(&mut self.stream)).await {
|
||||
Err(_) => Ok(None),
|
||||
Ok(Err(e)) => Err(ClientError::Io(e)),
|
||||
Ok(Ok(Frame::MatchEvent(ev))) => Ok(Some(ev)),
|
||||
Ok(Ok(Frame::Error(e))) => Err(ClientError::Server(e)),
|
||||
Ok(Ok(_)) => Err(ClientError::UnexpectedFrame {
|
||||
got: "non-event en await_event",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user