feat(brahman-handshake): Fase 1 — handshake brahman sobre stream libp2p
Segundo paso del plan "el encuentro entre Entes no se restringe a
local". El protocolo brahman (Hello / HelloAck / Ping / Pong /
MatchEvent / Farewell, frames postcard length-prefixed) ahora tambien
viaja sobre streams libp2p de la malla brahman-net — el mismo Init
acepta sesiones por Unix socket Y por libp2p indistintamente, y un
consumer remoto puede dial-ar al multiaddr y completar handshake.
Cambios:
- Session<S> y Client<S> genericos: ambos dejan de estar atados a
UnixStream y pasan a ser genericos sobre S: AsyncRead + AsyncWrite
+ Unpin + Send + 'static. El path Unix queda como
Client = Client<UnixStream> (default generico). Constructores
nuevos: Server::session_from_stream(stream),
Client::connect_with_stream(stream, card, wit).
- Refactor del post-handshake con split: tokio::select! sobre
&mut self.stream requeria S: Sync indirectamente, y libp2p::Stream
no es Sync. Reemplazado por tokio::io::split(stream) -> reader loop
principal + writer task separada que drena el push channel. Writer
compartido bajo Arc<Mutex<WriteHalf<S>>> para serializar Pong/Error
inline con los MatchEvents pusheados. Cleanup garantizado en todas
las ramas. La logica del post-handshake migra a funciones libres
(run_post_handshake, handle_inbound_frame, cleanup,
broadcast_match_diffs, do_handshake, register_session,
validate_hello).
- Nuevo modulo brahman-handshake::network:
- BRAHMAN_HANDSHAKE_PROTOCOL = "/brahman/handshake/1.0.0"
- LibP2pHandshakeStream = Compat<libp2p::Stream>
- run_libp2p_accept_loop(server, net): accept loop que delega cada
stream entrante a session_from_stream(stream.compat()). Sesiones
libp2p y Unix conviven en el mismo Server — comparten broker,
push table, last_matches.
- connect_libp2p(net, peer, card, wit): abre stream libp2p al peer
y arranca handshake.
- NetworkError tipado.
Deps: brahman-handshake gana brahman-net, futures, tokio-util.
brahman-net re-exporta Multiaddr, PeerId, Stream, StreamProtocol,
Protocol, OpenStreamError para que callers no necesiten dep directa
a libp2p.
Tests: 9 verdes en el path Unix (sin regresion). Nuevo
tests/network_libp2p.rs E2E que arma server con BrahmanNet, hace
listen TCP, monta accept loop; cliente con su propio BrahmanNet
dial-ea al peer_id, completa handshake remoto, ping, farewell.
Verifica que la sesion se registro durante la conversacion y se
removio tras farewell.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//! Test E2E: handshake brahman remoto sobre libp2p stream.
|
||||
//!
|
||||
//! Pipeline:
|
||||
//! 1. Server: bind Unix socket (necesario aunque no lo use el cliente);
|
||||
//! crear `BrahmanNet` y escuchar en `/ip4/127.0.0.1/tcp/0`;
|
||||
//! montar `run_libp2p_accept_loop`.
|
||||
//! 2. Client: crear su propio `BrahmanNet`; dial al multiaddr del
|
||||
//! server; `connect_libp2p` con su Card; `ping`; `farewell`.
|
||||
//! 3. Verificar: el server registró la sesión; sessions.len() == 1
|
||||
//! durante la sesión, == 0 después del farewell.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use brahman_broker::{Broker, BrokerConfig};
|
||||
use brahman_card::{
|
||||
ulid::Ulid, Card, CardKind, Lifecycle, Payload, Priority, Supervision,
|
||||
CARD_SCHEMA_VERSION,
|
||||
};
|
||||
use brahman_handshake::network::{connect_libp2p, run_libp2p_accept_loop};
|
||||
use brahman_handshake::server::{Server, ServerConfig};
|
||||
use brahman_net::{BrahmanNet, Multiaddr, PeerId, Protocol};
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
fn sample_card(label: &str) -> Card {
|
||||
Card {
|
||||
schema_version: CARD_SCHEMA_VERSION,
|
||||
id: Ulid::new(),
|
||||
label: label.into(),
|
||||
provides: BTreeSet::new(),
|
||||
requires: BTreeSet::new(),
|
||||
permissions: Default::default(),
|
||||
soma: Default::default(),
|
||||
payload: Payload::Virtual,
|
||||
supervision: Supervision::OneShot,
|
||||
lifecycle: Lifecycle::default(),
|
||||
priority: Priority::default(),
|
||||
kind: CardKind::Ente,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn libp2p_handshake_roundtrip() {
|
||||
// ---- Server side ----
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let unix_socket = tmp.path().join("brahman-init.sock");
|
||||
|
||||
let broker = Arc::new(Mutex::new(Broker::new(BrokerConfig::default())));
|
||||
let server = Arc::new(
|
||||
Server::bind(
|
||||
&unix_socket,
|
||||
ServerConfig {
|
||||
init_attached: true,
|
||||
broker: Some(broker.clone()),
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let sessions = server.sessions();
|
||||
|
||||
let server_net = Arc::new(BrahmanNet::new().unwrap());
|
||||
let server_peer_id = server_net.peer_id;
|
||||
|
||||
// Listen on a random TCP port.
|
||||
let listen_addr: Multiaddr = "/ip4/127.0.0.1/tcp/0".parse().unwrap();
|
||||
let actual_addr = server_net.listen(listen_addr).await;
|
||||
// Inject the libp2p PeerId into the multiaddr so the client knows
|
||||
// who to dial.
|
||||
let mut full_addr = actual_addr.clone();
|
||||
full_addr.push(Protocol::P2p(server_peer_id));
|
||||
|
||||
// Spawn the libp2p accept loop.
|
||||
tokio::spawn(run_libp2p_accept_loop(server.clone(), server_net.clone()));
|
||||
|
||||
// ---- Client side ----
|
||||
let client_net = BrahmanNet::new().unwrap();
|
||||
client_net.dial(full_addr.clone());
|
||||
|
||||
// Pequeña espera para que el dial conecte. En un entorno real el
|
||||
// caller usaría un mecanismo de barrier, pero para tests un sleep
|
||||
// corto es suficiente y deterministic en localhost.
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let card = sample_card("test.remote_ente");
|
||||
let mut client = connect_libp2p(&client_net, server_peer_id, card, None)
|
||||
.await
|
||||
.expect("handshake remoto debería completar");
|
||||
|
||||
// Verificación: el server vio la sesión.
|
||||
{
|
||||
let s = sessions.lock().await;
|
||||
assert_eq!(s.len(), 1, "una sesión registrada");
|
||||
let resolved = s.values().next().unwrap();
|
||||
assert_eq!(resolved.card.label, "test.remote_ente");
|
||||
}
|
||||
|
||||
// Ping roundtrip.
|
||||
let ts = client.ping().await.expect("ping debería responder");
|
||||
assert!(ts > 0, "timestamp del Pong > 0");
|
||||
|
||||
// Farewell limpio.
|
||||
client.farewell().await.expect("farewell debería completar");
|
||||
|
||||
// Tras el farewell, el cleanup remueve la sesión.
|
||||
// Damos un tick para que el handler procese el frame.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
{
|
||||
let s = sessions.lock().await;
|
||||
assert_eq!(s.len(), 0, "sesión removida tras farewell");
|
||||
}
|
||||
|
||||
// peer_id no usado aquí, pero validamos que la API existe.
|
||||
let _ = PeerId::random();
|
||||
}
|
||||
Reference in New Issue
Block a user