Write tests for docker integration, as docker is how we will initially handle distribution on heterogeneous hardware.
259 lines
8.8 KiB
Rust
259 lines
8.8 KiB
Rust
//! TCP transport with connection pooling and length-prefix framing.
|
|
//!
|
|
//! Wire format per envelope:
|
|
//! [4 bytes: total frame len (BE u32)]
|
|
//! [32 bytes: dest address]
|
|
//! [4 bytes: type_tag len (BE u32)]
|
|
//! [N bytes: type_tag UTF-8]
|
|
//! [remaining: payload bytes]
|
|
|
|
use std::collections::HashMap;
|
|
use std::io::{Read, Write};
|
|
use std::net::{SocketAddr, TcpListener, TcpStream};
|
|
use std::sync::Mutex;
|
|
|
|
use swactor::actor::ActorAddress;
|
|
use swactor::transport::{Transport, WireEnvelope};
|
|
use swactor::Error;
|
|
|
|
// ─── TcpTransport ──────────────────────────────────────────────────────────
|
|
|
|
/// TCP transport with connection pooling.
|
|
///
|
|
/// Maintains a pool of connections keyed by `SocketAddr`. Connections are
|
|
/// created on first use and reused for subsequent sends.
|
|
pub struct TcpTransport {
|
|
pool: Mutex<HashMap<SocketAddr, TcpStream>>,
|
|
/// Default destination for sends that don't specify an address.
|
|
/// Used when the transport is registered per-address in a TransportRouter.
|
|
default_dest: Option<SocketAddr>,
|
|
}
|
|
|
|
impl TcpTransport {
|
|
/// Create a transport that sends to a specific destination.
|
|
pub fn new(dest: SocketAddr) -> Self {
|
|
Self {
|
|
pool: Mutex::new(HashMap::new()),
|
|
default_dest: Some(dest),
|
|
}
|
|
}
|
|
|
|
/// Create a transport with no default destination.
|
|
/// The destination must be determined by the caller (e.g. via TransportRouter).
|
|
pub fn pool() -> Self {
|
|
Self {
|
|
pool: Mutex::new(HashMap::new()),
|
|
default_dest: None,
|
|
}
|
|
}
|
|
|
|
fn get_or_connect(&self, addr: SocketAddr) -> Result<TcpStream, Error> {
|
|
let mut pool = self.pool.lock().unwrap();
|
|
if let Some(stream) = pool.get(&addr) {
|
|
match stream.try_clone() {
|
|
Ok(s) => return Ok(s),
|
|
Err(_) => {
|
|
pool.remove(&addr);
|
|
}
|
|
}
|
|
}
|
|
let stream =
|
|
TcpStream::connect(addr).map_err(|e| Error::from(format!("TCP connect to {addr}: {e}")))?;
|
|
stream
|
|
.set_nodelay(true)
|
|
.map_err(|e| Error::from(format!("set_nodelay: {e}")))?;
|
|
pool.insert(addr, stream.try_clone().unwrap());
|
|
Ok(stream)
|
|
}
|
|
|
|
/// Send an envelope to a specific address.
|
|
///
|
|
/// If the write fails (e.g. stale connection from a dead peer), evicts
|
|
/// the pooled connection and retries once with a fresh one.
|
|
pub fn send_to(&self, addr: SocketAddr, envelope: WireEnvelope) -> Result<(), Error> {
|
|
let buf = encode_wire_envelope(&envelope);
|
|
let mut stream = self.get_or_connect(addr)?;
|
|
match stream.write_all(&buf) {
|
|
Ok(()) => Ok(()),
|
|
Err(_) => {
|
|
// Evict stale connection and retry once
|
|
self.pool.lock().unwrap().remove(&addr);
|
|
let mut stream = self.get_or_connect(addr)?;
|
|
stream
|
|
.write_all(&buf)
|
|
.map_err(|e| Error::from(format!("TCP send to {addr}: {e}")))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Transport for TcpTransport {
|
|
fn send(&self, envelope: WireEnvelope) -> Result<(), Error> {
|
|
let dest = self
|
|
.default_dest
|
|
.ok_or_else(|| Error::from("TcpTransport: no default destination"))?;
|
|
self.send_to(dest, envelope)
|
|
}
|
|
}
|
|
|
|
// ─── TcpListener wrapper ───────────────────────────────────────────────────
|
|
|
|
/// Accept loop that reads wire envelopes from incoming TCP connections.
|
|
pub struct TcpAcceptor {
|
|
listener: TcpListener,
|
|
}
|
|
|
|
impl TcpAcceptor {
|
|
/// Bind to a local address.
|
|
pub fn bind(addr: SocketAddr) -> Result<Self, Error> {
|
|
let listener =
|
|
TcpListener::bind(addr).map_err(|e| Error::from(format!("TCP bind {addr}: {e}")))?;
|
|
listener
|
|
.set_nonblocking(true)
|
|
.map_err(|e| Error::from(format!("set_nonblocking: {e}")))?;
|
|
Ok(Self { listener })
|
|
}
|
|
|
|
/// The local address this acceptor is bound to.
|
|
pub fn local_addr(&self) -> SocketAddr {
|
|
self.listener.local_addr().unwrap()
|
|
}
|
|
|
|
/// Non-blocking: accept new connections, read complete envelopes from them.
|
|
/// Returns all envelopes that could be read without blocking.
|
|
pub fn try_recv(&self, streams: &mut Vec<TcpStream>) -> Vec<(WireEnvelope, SocketAddr)> {
|
|
// Accept new connections
|
|
loop {
|
|
match self.listener.accept() {
|
|
Ok((stream, _peer)) => {
|
|
let _ = stream.set_nonblocking(true);
|
|
streams.push(stream);
|
|
}
|
|
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
|
|
Err(_) => break,
|
|
}
|
|
}
|
|
|
|
// Read from all streams
|
|
let mut envelopes = Vec::new();
|
|
let mut dead = Vec::new();
|
|
|
|
for (i, stream) in streams.iter_mut().enumerate() {
|
|
let peer = stream.peer_addr().unwrap_or_else(|_| "0.0.0.0:0".parse().unwrap());
|
|
loop {
|
|
match read_wire_envelope(stream) {
|
|
Ok(env) => envelopes.push((env, peer)),
|
|
Err(ReadError::WouldBlock) => break,
|
|
Err(ReadError::Disconnected) => {
|
|
dead.push(i);
|
|
break;
|
|
}
|
|
Err(ReadError::Other(_)) => {
|
|
dead.push(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove dead connections in reverse order
|
|
dead.sort_unstable();
|
|
dead.dedup();
|
|
for i in dead.into_iter().rev() {
|
|
streams.swap_remove(i);
|
|
}
|
|
|
|
envelopes
|
|
}
|
|
}
|
|
|
|
// ─── Wire format encoding/decoding ─────────────────────────────────────────
|
|
|
|
/// Encode a WireEnvelope to bytes in the length-prefixed wire format.
|
|
pub fn encode_wire_envelope(envelope: &WireEnvelope) -> Vec<u8> {
|
|
let tag_bytes = envelope.type_tag.as_bytes();
|
|
let frame_len: u32 = (32 + 4 + tag_bytes.len() + envelope.payload.len()) as u32;
|
|
|
|
let mut buf = Vec::with_capacity(4 + frame_len as usize);
|
|
buf.extend_from_slice(&frame_len.to_be_bytes());
|
|
buf.extend_from_slice(&envelope.dest.0);
|
|
buf.extend_from_slice(&(tag_bytes.len() as u32).to_be_bytes());
|
|
buf.extend_from_slice(tag_bytes);
|
|
buf.extend_from_slice(&envelope.payload);
|
|
buf
|
|
}
|
|
|
|
enum ReadError {
|
|
WouldBlock,
|
|
Disconnected,
|
|
Other(std::io::Error),
|
|
}
|
|
|
|
impl From<std::io::Error> for ReadError {
|
|
fn from(e: std::io::Error) -> Self {
|
|
match e.kind() {
|
|
std::io::ErrorKind::WouldBlock => ReadError::WouldBlock,
|
|
std::io::ErrorKind::UnexpectedEof => ReadError::Disconnected,
|
|
std::io::ErrorKind::ConnectionReset => ReadError::Disconnected,
|
|
_ => ReadError::Other(e),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read one WireEnvelope from a TCP stream.
|
|
fn read_wire_envelope(stream: &mut TcpStream) -> Result<WireEnvelope, ReadError> {
|
|
let mut len_buf = [0u8; 4];
|
|
stream.read_exact(&mut len_buf)?;
|
|
let frame_len = u32::from_be_bytes(len_buf) as usize;
|
|
|
|
let mut frame = vec![0u8; frame_len];
|
|
stream.read_exact(&mut frame)?;
|
|
|
|
let mut dest = [0u8; 32];
|
|
dest.copy_from_slice(&frame[0..32]);
|
|
|
|
let tag_len = u32::from_be_bytes(frame[32..36].try_into().unwrap()) as usize;
|
|
let type_tag = String::from_utf8_lossy(&frame[36..36 + tag_len]).to_string();
|
|
|
|
let payload = frame[36 + tag_len..].to_vec();
|
|
|
|
Ok(WireEnvelope {
|
|
dest: ActorAddress(dest),
|
|
type_tag,
|
|
payload,
|
|
})
|
|
}
|
|
|
|
/// Read a single envelope from a blocking stream. Public for use in tests/examples.
|
|
pub fn read_envelope_blocking(stream: &mut TcpStream) -> Result<WireEnvelope, Error> {
|
|
// Temporarily set blocking mode
|
|
stream
|
|
.set_nonblocking(false)
|
|
.map_err(|e| Error::from(format!("set_blocking: {e}")))?;
|
|
let mut len_buf = [0u8; 4];
|
|
stream
|
|
.read_exact(&mut len_buf)
|
|
.map_err(|e| Error::from(format!("read frame len: {e}")))?;
|
|
let frame_len = u32::from_be_bytes(len_buf) as usize;
|
|
|
|
let mut frame = vec![0u8; frame_len];
|
|
stream
|
|
.read_exact(&mut frame)
|
|
.map_err(|e| Error::from(format!("read frame: {e}")))?;
|
|
|
|
let mut dest = [0u8; 32];
|
|
dest.copy_from_slice(&frame[0..32]);
|
|
|
|
let tag_len = u32::from_be_bytes(frame[32..36].try_into().unwrap()) as usize;
|
|
let type_tag = String::from_utf8_lossy(&frame[36..36 + tag_len]).to_string();
|
|
|
|
let payload = frame[36 + tag_len..].to_vec();
|
|
|
|
let _ = stream.set_nonblocking(true);
|
|
|
|
Ok(WireEnvelope {
|
|
dest: ActorAddress(dest),
|
|
type_tag,
|
|
payload,
|
|
})
|
|
}
|