#![cfg_attr(not(test), deny(clippy::disallowed_methods))]
#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::collections::{BTreeMap, BTreeSet};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use super::wire::{LENGTH_PREFIX_BYTES, RejectReason, frame_len, framed};
use crate::AutumnResult;
use crate::entropy::Entropy;
pub type PeerAddr = String;
pub type IncomingFrames = mpsc::Receiver<(PeerAddr, Vec<u8>)>;
pub const PEER_QUEUE_CAPACITY: usize = 64;
pub const FAREWELL_LANE_CAPACITY: usize = 2;
pub const RECONNECT_BACKOFF_MIN: Duration = Duration::from_millis(50);
pub const RECONNECT_BACKOFF_MAX: Duration = Duration::from_secs(2);
const ACCEPT_ERROR_BACKOFF: Duration = Duration::from_millis(10);
const DIAL_TIMEOUT: Duration = RECONNECT_BACKOFF_MAX;
const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
pub const MAX_INBOUND_CONNECTIONS: usize = 128;
pub const DEFAULT_INBOUND_IDLE_TIMEOUT: Duration = Duration::from_secs(10);
pub const MAX_UNAUTHENTICATED_FRAMES: u32 = 3;
const WRITER_DRAIN_GRACE: Duration = super::node::LEAVE_BUDGET;
pub trait PeerTransport: Send + Sync + 'static {
fn send(&self, to: &str, frame: Vec<u8>);
fn send_farewell(&self, to: &str, frame: Vec<u8>) -> bool {
self.send(to, frame);
true
}
fn take_incoming(&self) -> Option<IncomingFrames>;
fn local_addr(&self) -> SocketAddr;
fn start(&self, shutdown: &CancellationToken, entropy: &Arc<dyn Entropy>) {
let _ = (shutdown, entropy);
}
fn pending_frames(&self) -> usize {
0
}
fn dropped_frames(&self) -> u64 {
0
}
fn framing_rejections(&self) -> u64 {
0
}
fn note_unauthenticated_frame(&self, from: &str) {
let _ = from;
}
fn retain_peers(&self, live: &BTreeSet<String>) {
let _ = live;
}
}
struct TransportIo {
runtime: tokio::runtime::Handle,
writers: CancellationToken,
entropy: Arc<dyn Entropy>,
}
#[derive(Clone)]
struct InboundLimits {
idle_timeout: Duration,
live: Arc<AtomicUsize>,
framing_rejections: Arc<AtomicU64>,
connections: Arc<InboundConnections>,
}
struct TrackedConnection {
id: u64,
unauthenticated: u32,
close: CancellationToken,
}
#[derive(Default)]
struct InboundConnections {
live: Mutex<BTreeMap<PeerAddr, TrackedConnection>>,
next_id: AtomicU64,
}
impl InboundConnections {
fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<PeerAddr, TrackedConnection>> {
self.live.lock().unwrap_or_else(PoisonError::into_inner)
}
fn register(self: &Arc<Self>, peer: &str, close: CancellationToken) -> InboundRegistration {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
self.lock().insert(
peer.to_owned(),
TrackedConnection {
id,
unauthenticated: 0,
close,
},
);
InboundRegistration {
connections: Arc::clone(self),
peer: peer.to_owned(),
id,
}
}
fn note_unauthenticated(&self, peer: &str) {
let mut live = self.lock();
let Some(tracked) = live.get_mut(peer) else {
return;
};
tracked.unauthenticated = tracked.unauthenticated.saturating_add(1);
if tracked.unauthenticated < MAX_UNAUTHENTICATED_FRAMES {
return;
}
tracked.close.cancel();
drop(live);
tracing::warn!(
peer = %peer,
budget = MAX_UNAUTHENTICATED_FRAMES,
"cluster: inbound connection spent its unauthenticated-frame budget \
without ever proving the shared secret; closing it"
);
}
}
struct InboundRegistration {
connections: Arc<InboundConnections>,
peer: PeerAddr,
id: u64,
}
impl Drop for InboundRegistration {
fn drop(&mut self) {
let mut live = self.connections.lock();
if live
.get(&self.peer)
.is_some_and(|tracked| tracked.id == self.id)
{
live.remove(&self.peer);
}
}
}
struct InboundSlot(Arc<AtomicUsize>);
impl Drop for InboundSlot {
fn drop(&mut self) {
let _ = self
.0
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |live| {
Some(live.saturating_sub(1))
});
}
}
#[derive(Clone)]
struct PeerLanes {
pushes: mpsc::Sender<Vec<u8>>,
farewell: mpsc::Sender<Vec<u8>>,
}
impl PeerLanes {
fn queued(&self) -> usize {
Self::depth(&self.pushes).saturating_add(Self::depth(&self.farewell))
}
fn depth(lane: &mpsc::Sender<Vec<u8>>) -> usize {
lane.max_capacity().saturating_sub(lane.capacity())
}
}
pub struct TcpPeerTransport {
local_addr: SocketAddr,
listener: Mutex<Option<std::net::TcpListener>>,
incoming: Mutex<Option<IncomingFrames>>,
inbound_tx: mpsc::Sender<(PeerAddr, Vec<u8>)>,
peers: Mutex<BTreeMap<PeerAddr, PeerLanes>>,
io: Mutex<Option<TransportIo>>,
dropped: AtomicU64,
in_flight: Arc<AtomicUsize>,
limits: InboundLimits,
}
impl std::fmt::Debug for TcpPeerTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TcpPeerTransport")
.field("local_addr", &self.local_addr)
.finish_non_exhaustive()
}
}
impl TcpPeerTransport {
pub fn bind(addr: &str) -> AutumnResult<Self> {
let listener =
std::net::TcpListener::bind(addr).map_err(|err| super::bind_error(addr, &err))?;
let local_addr = listener
.local_addr()
.map_err(|err| super::bind_error(addr, &err))?;
listener
.set_nonblocking(true)
.map_err(|err| super::bind_error(addr, &err))?;
let (inbound_tx, incoming) = mpsc::channel(PEER_QUEUE_CAPACITY);
Ok(Self {
local_addr,
listener: Mutex::new(Some(listener)),
incoming: Mutex::new(Some(incoming)),
inbound_tx,
peers: Mutex::new(BTreeMap::new()),
io: Mutex::new(None),
dropped: AtomicU64::new(0),
in_flight: Arc::new(AtomicUsize::new(0)),
limits: InboundLimits {
idle_timeout: DEFAULT_INBOUND_IDLE_TIMEOUT,
live: Arc::new(AtomicUsize::new(0)),
framing_rejections: Arc::new(AtomicU64::new(0)),
connections: Arc::new(InboundConnections::default()),
},
})
}
#[must_use]
pub const fn with_inbound_idle_timeout(mut self, idle_timeout: Duration) -> Self {
self.limits.idle_timeout = idle_timeout;
self
}
pub fn take_listener(&self) -> Option<std::net::TcpListener> {
self.listener
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
}
pub fn inbound_sender(&self) -> mpsc::Sender<(PeerAddr, Vec<u8>)> {
self.inbound_tx.clone()
}
fn lock_peers(&self) -> std::sync::MutexGuard<'_, BTreeMap<PeerAddr, PeerLanes>> {
self.peers.lock().unwrap_or_else(PoisonError::into_inner)
}
fn lock_io(&self) -> std::sync::MutexGuard<'_, Option<TransportIo>> {
self.io.lock().unwrap_or_else(PoisonError::into_inner)
}
fn spawn_context(
&self,
) -> Option<(tokio::runtime::Handle, CancellationToken, Arc<dyn Entropy>)> {
self.lock_io().as_ref().map(|io| {
(
io.runtime.clone(),
io.writers.child_token(),
Arc::clone(&io.entropy),
)
})
}
#[cfg(test)]
fn live_inbound(&self) -> usize {
self.limits.live.load(Ordering::Relaxed)
}
#[cfg(test)]
fn writer_count(&self) -> usize {
self.lock_peers().len()
}
fn writer_for(&self, to: &str) -> Option<PeerLanes> {
let mut peers = self.lock_peers();
if peers.get(to).is_some_and(|lanes| lanes.pushes.is_closed()) {
peers.remove(to);
}
if let Some(existing) = peers.get(to) {
return Some(existing.clone());
}
let (runtime, shutdown, entropy) = self.spawn_context()?;
let (pushes, queue) = mpsc::channel(PEER_QUEUE_CAPACITY);
let (farewell, departure) = mpsc::channel(FAREWELL_LANE_CAPACITY);
runtime.spawn(peer_writer(
to.to_owned(),
queue,
departure,
shutdown,
entropy,
Arc::clone(&self.in_flight),
));
let lanes = PeerLanes { pushes, farewell };
peers.insert(to.to_owned(), lanes.clone());
drop(peers);
Some(lanes)
}
}
impl PeerTransport for TcpPeerTransport {
fn send(&self, to: &str, frame: Vec<u8>) {
let Some(lanes) = self.writer_for(to) else {
self.dropped.fetch_add(1, Ordering::Relaxed);
return;
};
if lanes.pushes.try_send(frame).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
tracing::debug!(
peer = %to,
"cluster: peer send queue full or closed, dropping a state push \
(anti-entropy re-sends the document)"
);
}
}
fn send_farewell(&self, to: &str, frame: Vec<u8>) -> bool {
let Some(lanes) = self.writer_for(to) else {
self.dropped.fetch_add(1, Ordering::Relaxed);
return false;
};
if lanes.farewell.try_send(frame).is_ok() {
return true;
}
self.dropped.fetch_add(1, Ordering::Relaxed);
false
}
fn take_incoming(&self) -> Option<IncomingFrames> {
self.incoming
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
}
fn local_addr(&self) -> SocketAddr {
self.local_addr
}
fn start(&self, shutdown: &CancellationToken, entropy: &Arc<dyn Entropy>) {
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
tracing::warn!(
"cluster: the peer transport was started outside a Tokio runtime; \
no cluster I/O will run"
);
return;
};
let writers = CancellationToken::new();
{
let mut io = self.lock_io();
if io.is_some() {
return;
}
*io = Some(TransportIo {
runtime: runtime.clone(),
writers: writers.clone(),
entropy: Arc::clone(entropy),
});
}
runtime.spawn(retire_writers(shutdown.clone(), writers));
let Some(listener) = self.take_listener() else {
return;
};
runtime.spawn(accept_loop(
listener,
self.inbound_sender(),
shutdown.child_token(),
self.limits.clone(),
));
}
fn pending_frames(&self) -> usize {
let queued: usize = self.lock_peers().values().map(PeerLanes::queued).sum();
queued.saturating_add(self.in_flight.load(Ordering::Relaxed))
}
fn dropped_frames(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
fn framing_rejections(&self) -> u64 {
self.limits.framing_rejections.load(Ordering::Relaxed)
}
fn note_unauthenticated_frame(&self, from: &str) {
self.limits.connections.note_unauthenticated(from);
}
fn retain_peers(&self, live: &BTreeSet<String>) {
self.lock_peers().retain(|addr, _| live.contains(addr));
}
}
async fn retire_writers(shutdown: CancellationToken, writers: CancellationToken) {
shutdown.cancelled().await;
tokio::time::sleep(WRITER_DRAIN_GRACE).await;
writers.cancel();
}
async fn accept_loop(
listener: std::net::TcpListener,
inbound: mpsc::Sender<(PeerAddr, Vec<u8>)>,
shutdown: CancellationToken,
limits: InboundLimits,
) {
let Ok(listener) = tokio::net::TcpListener::from_std(listener) else {
tracing::warn!("cluster: could not adopt the bound listener; no peer can connect");
return;
};
loop {
let accepted = tokio::select! {
result = listener.accept() => result,
() = shutdown.cancelled() => return,
};
match accepted {
Ok((stream, peer)) => {
let Some(slot) = claim_inbound_slot(&limits.live) else {
drop(stream);
tracing::warn!(
peer = %peer,
cap = MAX_INBOUND_CONNECTIONS,
"cluster: inbound connection cap reached; closing the new connection"
);
continue;
};
let peer = peer.to_string();
let close = shutdown.child_token();
let registration = limits.connections.register(&peer, close.clone());
let reader = connection_reader(
stream,
peer,
inbound.clone(),
close,
limits.clone(),
slot,
registration,
);
tokio::spawn(reader);
}
Err(err) => {
tracing::debug!(error = %err, "cluster: accept failed; the listener keeps running");
tokio::select! {
() = tokio::time::sleep(ACCEPT_ERROR_BACKOFF) => {}
() = shutdown.cancelled() => return,
}
}
}
}
}
fn claim_inbound_slot(live: &Arc<AtomicUsize>) -> Option<InboundSlot> {
live.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |live| {
(live < MAX_INBOUND_CONNECTIONS).then(|| live.saturating_add(1))
})
.ok()
.map(|_| InboundSlot(Arc::clone(live)))
}
async fn connection_reader(
mut stream: tokio::net::TcpStream,
peer: PeerAddr,
inbound: mpsc::Sender<(PeerAddr, Vec<u8>)>,
shutdown: CancellationToken,
limits: InboundLimits,
slot: InboundSlot,
registration: InboundRegistration,
) {
let _slot = slot;
let _registration = registration;
loop {
let mut prefix = [0u8; LENGTH_PREFIX_BYTES];
let read = tokio::select! {
result = tokio::time::timeout(
limits.idle_timeout,
stream.read_exact(&mut prefix),
) => result,
() = shutdown.cancelled() => return,
};
match read {
Ok(Ok(_)) => {}
Ok(Err(_)) => return,
Err(_elapsed) => {
tracing::debug!(
peer = %peer,
idle_ms = limits.idle_timeout.as_millis(),
"cluster: inbound connection delivered no frame within its idle \
deadline; closing it"
);
return;
}
}
let Some(declared) = frame_len(prefix) else {
limits.framing_rejections.fetch_add(1, Ordering::Relaxed);
tracing::warn!(
peer = %peer,
reason = RejectReason::Oversize.label(),
"cluster: illegal frame length prefix; closing the connection"
);
return;
};
let mut body = vec![0u8; declared];
let read = tokio::select! {
result = tokio::time::timeout(
limits.idle_timeout,
stream.read_exact(&mut body),
) => result,
() = shutdown.cancelled() => return,
};
if !matches!(read, Ok(Ok(_))) {
return;
}
let handed_up = tokio::select! {
result = inbound.send((peer.clone(), framed(prefix, &body))) => result,
() = shutdown.cancelled() => return,
};
if handed_up.is_err() {
return;
}
}
}
struct InFlightFrame(Arc<AtomicUsize>);
impl InFlightFrame {
fn claim(counter: &Arc<AtomicUsize>) -> Self {
counter.fetch_add(1, Ordering::Relaxed);
Self(Arc::clone(counter))
}
}
impl Drop for InFlightFrame {
fn drop(&mut self) {
let _ = self
.0
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |held| {
Some(held.saturating_sub(1))
});
}
}
async fn peer_writer(
to: PeerAddr,
mut queue: mpsc::Receiver<Vec<u8>>,
mut departure: mpsc::Receiver<Vec<u8>>,
shutdown: CancellationToken,
entropy: Arc<dyn Entropy>,
in_flight: Arc<AtomicUsize>,
) {
let mut connection: Option<tokio::net::TcpStream> = None;
let mut backoff = RECONNECT_BACKOFF_MIN;
loop {
let queued = tokio::select! {
biased;
Some(frame) = departure.recv() => Some(frame),
frame = queue.recv() => frame,
() = shutdown.cancelled() => None,
};
let Some(frame) = queued else { return };
let _held = InFlightFrame::claim(&in_flight);
if connection.is_none() {
let dialled = tokio::select! {
result = tokio::time::timeout(
DIAL_TIMEOUT,
tokio::net::TcpStream::connect(&to),
) => result.ok().and_then(Result::ok),
() = shutdown.cancelled() => return,
};
if let Some(stream) = dialled {
backoff = RECONNECT_BACKOFF_MIN;
connection = Some(stream);
} else {
tokio::select! {
() = tokio::time::sleep(super::jittered(backoff, entropy.as_ref())) => {}
() = shutdown.cancelled() => return,
}
backoff = backoff.saturating_mul(2).min(RECONNECT_BACKOFF_MAX);
continue;
}
}
if let Some(stream) = connection.as_mut() {
let written = tokio::select! {
result = tokio::time::timeout(WRITE_TIMEOUT, stream.write_all(&frame)) => result,
() = shutdown.cancelled() => return,
};
if !matches!(written, Ok(Ok(()))) {
connection = None;
}
}
}
}
#[cfg(test)]
mod tcp_tests {
use super::{
DIAL_TIMEOUT, FAREWELL_LANE_CAPACITY, MAX_INBOUND_CONNECTIONS, MAX_UNAUTHENTICATED_FRAMES,
PEER_QUEUE_CAPACITY, PeerTransport as _, TcpPeerTransport,
};
use crate::entropy::SeededEntropy;
use std::collections::BTreeSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
use tokio_util::sync::CancellationToken;
const IDLE: Duration = Duration::from_millis(200);
async fn poll_until(mut condition: impl FnMut() -> bool) {
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
while !condition() && tokio::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(5)).await;
}
}
fn started(idle: Duration) -> (Arc<TcpPeerTransport>, CancellationToken) {
let transport = Arc::new(
TcpPeerTransport::bind("127.0.0.1:0")
.expect("binding an ephemeral loopback port must succeed")
.with_inbound_idle_timeout(idle),
);
let token = CancellationToken::new();
let entropy: Arc<dyn crate::entropy::Entropy> = Arc::new(SeededEntropy::new(7));
transport.start(&token, &entropy);
(transport, token)
}
fn unauthenticated_frame() -> Vec<u8> {
let body = b"not-an-envelope";
let mut frame = u32::try_from(body.len())
.unwrap_or(u32::MAX)
.to_be_bytes()
.to_vec();
frame.extend_from_slice(body);
frame
}
fn spawn_rejecting_consumer(transport: &Arc<TcpPeerTransport>) -> Arc<AtomicU32> {
let mut incoming = transport
.take_incoming()
.expect("the inbound stream must still be available");
let transport = Arc::clone(transport);
let reported = Arc::new(AtomicU32::new(0));
let counter = Arc::clone(&reported);
tokio::spawn(async move {
while let Some((from, _frame)) = incoming.recv().await {
transport.note_unauthenticated_frame(&from);
counter.fetch_add(1, Ordering::Relaxed);
}
});
reported
}
#[tokio::test(flavor = "multi_thread")]
async fn inbound_connection_is_closed_once_it_spends_its_unauthenticated_budget() {
let (transport, token) = started(Duration::from_secs(30));
let reported = spawn_rejecting_consumer(&transport);
let mut client = tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("the cluster listener must accept a connection");
for _ in 1..MAX_UNAUTHENTICATED_FRAMES {
client
.write_all(&unauthenticated_frame())
.await
.expect("writing a well-framed garbage frame must reach the node");
}
let under_budget = MAX_UNAUTHENTICATED_FRAMES.saturating_sub(1);
poll_until(|| reported.load(Ordering::Relaxed) >= under_budget).await;
let mut sink = [0_u8; 1];
let still_open =
tokio::time::timeout(Duration::from_millis(200), client.read(&mut sink)).await;
assert!(
still_open.is_err(),
"a connection that has not yet spent its budget of \
{MAX_UNAUTHENTICATED_FRAMES} must stay open — a bound that fires \
early would cut off a peer mid-secret-rotation before its bad MAC \
could even be diagnosed; observed {still_open:?}"
);
client
.write_all(&unauthenticated_frame())
.await
.expect("writing a well-framed garbage frame must reach the node");
let closed = tokio::time::timeout(Duration::from_secs(5), client.read(&mut sink)).await;
assert!(
matches!(closed, Ok(Ok(0))),
"after {MAX_UNAUTHENTICATED_FRAMES} frames that never proved the \
shared secret the node must close the connection, or the inbound \
cap is a budget anyone who can reach the port can hold forever; \
observed {closed:?}"
);
poll_until(|| transport.live_inbound() == 0).await;
assert_eq!(
transport.live_inbound(),
0,
"closing the connection must give its slot back"
);
let mut fresh = tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("the listener must still accept connections after closing an abuser");
fresh
.write_all(&unauthenticated_frame())
.await
.expect("a fresh connection must still be able to deliver a frame");
let delivered = MAX_UNAUTHENTICATED_FRAMES.saturating_add(1);
poll_until(|| reported.load(Ordering::Relaxed) >= delivered).await;
assert_eq!(
reported.load(Ordering::Relaxed),
delivered,
"the listener must keep accepting and delivering frames; a bound \
that took the whole accept loop down with the connection would be \
a worse denial of service than the one it fixes"
);
token.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn inbound_cap_becomes_available_again_once_abusers_are_closed() {
let (transport, token) = started(Duration::from_secs(30));
let reported = spawn_rejecting_consumer(&transport);
let mut abusers = Vec::with_capacity(MAX_INBOUND_CONNECTIONS);
for _ in 0..MAX_INBOUND_CONNECTIONS {
abusers.push(
tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("the cluster listener must accept a connection"),
);
}
poll_until(|| transport.live_inbound() == MAX_INBOUND_CONNECTIONS).await;
assert_eq!(
transport.live_inbound(),
MAX_INBOUND_CONNECTIONS,
"sanity: the budget must actually be full, or the refusal below \
proves nothing"
);
let mut refused = tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("a connection at the cap is accepted and then closed");
let mut sink = [0_u8; 1];
let at_cap = tokio::time::timeout(Duration::from_secs(5), refused.read(&mut sink)).await;
assert!(
matches!(at_cap, Ok(Ok(0))),
"sanity: at the cap a new connection must be closed immediately; \
observed {at_cap:?}"
);
for abuser in &mut abusers {
for _ in 0..MAX_UNAUTHENTICATED_FRAMES {
abuser
.write_all(&unauthenticated_frame())
.await
.expect("writing a well-framed garbage frame must reach the node");
}
}
poll_until(|| transport.live_inbound() == 0).await;
assert_eq!(
transport.live_inbound(),
0,
"every connection that spent its budget must be closed, or a host \
that cannot produce a valid MAC keeps the port to itself"
);
let before = reported.load(Ordering::Relaxed);
let mut peer = tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("the freed budget must accept a new connection");
peer.write_all(&unauthenticated_frame())
.await
.expect("the new connection must be able to deliver a frame");
poll_until(|| reported.load(Ordering::Relaxed) > before).await;
assert!(
reported.load(Ordering::Relaxed) > before,
"a connection accepted after the abusers were closed must be read \
from, not merely accepted"
);
drop(abusers);
token.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn silent_inbound_connection_is_closed_at_the_idle_deadline() {
let (transport, token) = started(IDLE);
let mut client = tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("the cluster listener must accept a connection");
let mut sink = [0_u8; 1];
let closed = tokio::time::timeout(Duration::from_secs(5), client.read(&mut sink)).await;
assert!(
matches!(closed, Ok(Ok(0))),
"a connection that delivers no frame within its idle deadline must be \
closed by the node, not parked forever; observed {closed:?}"
);
token.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn inbound_connection_slots_are_released_when_connections_close() {
let (transport, token) = started(Duration::from_secs(30));
let mut clients = Vec::new();
for _ in 0_u8..4 {
clients.push(
tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("the cluster listener must accept a connection"),
);
}
poll_until(|| transport.live_inbound() == 4).await;
assert_eq!(
transport.live_inbound(),
4,
"every accepted connection must take one slot in the budget"
);
drop(clients);
poll_until(|| transport.live_inbound() == 0).await;
assert_eq!(
transport.live_inbound(),
0,
"a closed connection must give its slot back, or the node stops \
accepting anything once it has seen {MAX_INBOUND_CONNECTIONS} \
connections in its life"
);
token.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn oversize_length_prefix_is_counted_and_closes_the_connection() {
let (transport, token) = started(Duration::from_secs(30));
let mut client = tokio::net::TcpStream::connect(transport.local_addr())
.await
.expect("the cluster listener must accept a connection");
assert_eq!(
transport.framing_rejections(),
0,
"sanity: nothing has been rejected yet"
);
client
.write_all(&u32::MAX.to_be_bytes())
.await
.expect("writing a hostile length prefix must reach the node");
let mut sink = [0_u8; 1];
let closed = tokio::time::timeout(Duration::from_secs(5), client.read(&mut sink)).await;
assert!(
matches!(closed, Ok(Ok(0))),
"a 4 GiB length prefix desynchronizes the framing, so the connection \
must close; observed {closed:?}"
);
assert_eq!(
transport.framing_rejections(),
1,
"the oversize rejection must be counted even though the frame never \
reaches the verifier"
);
token.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn pending_frames_counts_the_frame_a_writer_still_holds() {
const BLACKHOLE: &str = "192.0.2.1:9";
let (transport, token) = started(Duration::from_secs(30));
assert!(
DIAL_TIMEOUT >= Duration::from_secs(1),
"the settle window below assumes a dial parks for at least 1s"
);
transport.send(BLACKHOLE, vec![0_u8; 32]);
tokio::time::sleep(Duration::from_millis(500)).await;
if transport.pending_frames() == 0 {
eprintln!("skipping: this network fails TEST-NET dials fast");
token.cancel();
return;
}
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(
transport.pending_frames(),
1,
"a frame the writer took off its queue and has not written yet must \
still count as pending — reporting 0 here tells the departure flush \
the Leave is on the wire when it is not"
);
token.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn pending_frames_includes_a_claimed_in_flight_frame() {
let (transport, token) = started(Duration::from_secs(30));
assert_eq!(
transport.pending_frames(),
0,
"a fresh transport holds nothing"
);
let held = super::InFlightFrame::claim(&transport.in_flight);
assert_eq!(
transport.pending_frames(),
1,
"a claimed in-flight frame must count as pending: queue depth alone \
reports a frame as flushed while its writer still owes the OS a write"
);
drop(held);
assert_eq!(
transport.pending_frames(),
0,
"dropping the claim must return the count — written, dropped, and \
abandoned frames all release it on the same guard"
);
token.cancel();
}
#[tokio::test]
async fn a_departure_is_queued_even_when_the_peer_queue_is_full() {
const PEER: &str = "127.0.0.1:9";
let (transport, token) = started(IDLE);
for _ in 0..PEER_QUEUE_CAPACITY {
transport.send(PEER, vec![0_u8; 8]);
}
assert_eq!(
transport.dropped_frames(),
0,
"sanity: a queue of {PEER_QUEUE_CAPACITY} must hold \
{PEER_QUEUE_CAPACITY} frames"
);
transport.send(PEER, vec![0_u8; 8]);
assert_eq!(
transport.dropped_frames(),
1,
"sanity: the queue must now be full and dropping, or the farewell \
below is not being offered the case it exists for"
);
for frame in 0..FAREWELL_LANE_CAPACITY {
assert!(
transport.send_farewell(PEER, vec![1_u8; 8]),
"frame {frame} of a departure must be accepted while the peer's \
push queue is full — the farewell is the one frame nothing \
re-sends"
);
}
assert_eq!(
transport.dropped_frames(),
1,
"…and neither frame may cost a drop"
);
assert_eq!(
transport.pending_frames(),
PEER_QUEUE_CAPACITY.saturating_add(FAREWELL_LANE_CAPACITY),
"a farewell waiting in the departure lane must count as pending, or \
the departure flush returns before it has reached the wire"
);
assert!(
!transport.send_farewell(PEER, vec![2_u8; 8]),
"a departure lane holding a whole departure must refuse a third \
frame instead of pretending to have taken it"
);
assert_eq!(
transport.dropped_frames(),
2,
"…and that refusal must be counted"
);
token.cancel();
}
#[tokio::test]
async fn the_writer_takes_the_departure_lane_before_anything_queued() {
const STALE: u8 = 0x11;
const FAREWELL: u8 = 0x22;
const FRAME_BYTES: usize = 8;
let peer = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("binding an ephemeral loopback port must succeed");
let peer_addr = peer
.local_addr()
.expect("the peer listener must report its address")
.to_string();
let (transport, token) = started(IDLE);
for _ in 0..8_u8 {
transport.send(&peer_addr, vec![STALE; FRAME_BYTES]);
}
assert!(
transport.send_farewell(&peer_addr, vec![FAREWELL; FRAME_BYTES]),
"sanity: the departure lane must accept the farewell"
);
let (mut accepted, _) = tokio::time::timeout(Duration::from_secs(5), peer.accept())
.await
.expect("the writer must dial its peer")
.expect("accepting the writer's connection must succeed");
let mut first = [0_u8; FRAME_BYTES];
tokio::time::timeout(Duration::from_secs(5), accepted.read_exact(&mut first))
.await
.expect("the writer must write a frame")
.expect("reading the writer's first frame must succeed");
assert_eq!(
first, [FAREWELL; FRAME_BYTES],
"the first frame on the wire must be the departure, not a queued \
state push: a farewell that waits its turn behind a stalled peer's \
queue is one the peer never hears, and the survivor falls back to \
the suspicion timeout it was supposed to be spared"
);
token.cancel();
}
#[tokio::test(flavor = "multi_thread")]
async fn writers_retire_when_an_address_leaves_the_target_set() {
let (transport, token) = started(IDLE);
transport.send("127.0.0.1:9", vec![1, 2, 3]);
transport.send("127.0.0.1:10", vec![4, 5, 6]);
assert_eq!(
transport.writer_count(),
2,
"each addressed peer must get its own writer queue"
);
let live: BTreeSet<String> = std::iter::once("127.0.0.1:10".to_owned()).collect();
transport.retain_peers(&live);
assert_eq!(
transport.writer_count(),
1,
"an address that has left the target set must not keep a writer \
queue alive — repeated address churn would otherwise accumulate \
one task per address, forever"
);
token.cancel();
}
}
#[cfg(test)]
pub use loopback::LoopbackRouter;
#[cfg(test)]
mod loopback {
use super::{IncomingFrames, PEER_QUEUE_CAPACITY, PeerAddr, PeerTransport};
use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex, PoisonError};
use tokio::sync::mpsc;
const FIRST_LOOPBACK_PORT: u16 = 47_000;
#[derive(Default)]
struct RouterInner {
issued: u16,
peers: BTreeMap<PeerAddr, mpsc::Sender<(PeerAddr, Vec<u8>)>>,
}
#[derive(Clone, Default)]
pub struct LoopbackRouter {
inner: Arc<Mutex<RouterInner>>,
}
impl LoopbackRouter {
pub fn new() -> Self {
Self::default()
}
fn lock(&self) -> std::sync::MutexGuard<'_, RouterInner> {
self.inner.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn endpoint(&self) -> Arc<LoopbackTransport> {
let (tx, rx) = mpsc::channel(PEER_QUEUE_CAPACITY);
let addr = {
let mut guard = self.lock();
let port = FIRST_LOOPBACK_PORT.saturating_add(guard.issued);
guard.issued = guard.issued.saturating_add(1);
let addr = SocketAddr::from(([127, 0, 0, 1], port));
guard.peers.insert(addr.to_string(), tx);
addr
};
Arc::new(LoopbackTransport {
router: self.clone(),
addr,
incoming: Mutex::new(Some(rx)),
})
}
pub fn deliver(&self, from: &str, to: &str, frame: Vec<u8>) -> bool {
let sender = {
let guard = self.lock();
if !guard.peers.contains_key(from) {
return false;
}
guard.peers.get(to).cloned()
};
sender.is_some_and(|tx| tx.try_send((from.to_owned(), frame)).is_ok())
}
pub fn disconnect(&self, addr: &str) {
self.lock().peers.remove(addr);
}
}
pub struct LoopbackTransport {
router: LoopbackRouter,
addr: SocketAddr,
incoming: Mutex<Option<IncomingFrames>>,
}
impl std::fmt::Debug for LoopbackTransport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LoopbackTransport")
.field("addr", &self.addr)
.finish_non_exhaustive()
}
}
impl PeerTransport for LoopbackTransport {
fn send(&self, to: &str, frame: Vec<u8>) {
let _delivered = self.router.deliver(&self.addr.to_string(), to, frame);
}
fn send_farewell(&self, to: &str, frame: Vec<u8>) -> bool {
self.router.deliver(&self.addr.to_string(), to, frame)
}
fn take_incoming(&self) -> Option<IncomingFrames> {
self.incoming
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take()
}
fn local_addr(&self) -> SocketAddr {
self.addr
}
}
}