feat(relay): clean up peers on leave

Add explicit room leave semantics for future relay connection tasks. Disconnect
handling will need to remove peers from room membership without reaching into the
room internals or leaving stale MAC indexes behind.

Leaving a client now removes both its peer entry and MAC mapping so the same MAC
can rejoin later. Leaving a gateway clears gateway occupancy while preserving any
remaining clients. If the last peer leaves, the room is removed from the
registry. The result reports which peer left and whether the room was removed so
the networking layer can emit lifecycle events cleanly.

Test Plan:
- cargo fmt --check
- cargo test --workspace
- cargo clippy --workspace --all-targets -- -D warnings

Refs: PLAN.md relay disconnect reason and reconnect handling groundwork
This commit is contained in:
2026-05-21 17:41:16 +02:00
parent 77894c4706
commit 81ad7abe84
2 changed files with 125 additions and 0 deletions
+124
View File
@@ -73,6 +73,29 @@ impl RoomSnapshot {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeaveResult {
peer: PeerInfo,
room_removed: bool,
}
impl LeaveResult {
#[must_use]
const fn new(peer: PeerInfo, room_removed: bool) -> Self {
Self { peer, room_removed }
}
#[must_use]
pub const fn peer(&self) -> &PeerInfo {
&self.peer
}
#[must_use]
pub const fn room_removed(&self) -> bool {
self.room_removed
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForwardingDecision {
targets: Vec<u32>,
@@ -183,6 +206,21 @@ impl RoomRegistry {
self.rooms.get(room).map(Room::snapshot)
}
pub fn leave(&mut self, room: &RoomCode, peer_id: u32) -> Result<LeaveResult, ForwardingError> {
let room_state = self
.rooms
.get_mut(room)
.ok_or_else(|| ForwardingError::UnknownRoom(room.clone()))?;
let peer = room_state.leave(room, peer_id)?;
let room_removed = room_state.is_empty();
if room_removed {
self.rooms.remove(room);
}
Ok(LeaveResult::new(peer, room_removed))
}
pub fn forward_ethernet(
&self,
room: &RoomCode,
@@ -331,6 +369,32 @@ impl Room {
}
}
fn leave(&mut self, room_code: &RoomCode, peer_id: u32) -> Result<PeerInfo, ForwardingError> {
if self
.gateway
.as_ref()
.is_some_and(|gateway| gateway.peer_id() == peer_id)
{
return Ok(self.gateway.take().expect("gateway exists"));
}
let peer =
self.clients
.remove(&peer_id)
.ok_or_else(|| ForwardingError::UnknownIngressPeer {
room: room_code.clone(),
peer_id,
})?;
let mac = peer.mac().expect("client peer info has MAC");
self.clients_by_mac.remove(&mac);
Ok(peer)
}
fn is_empty(&self) -> bool {
self.gateway.is_none() && self.clients.is_empty()
}
fn forward_ethernet(
&self,
room_code: &RoomCode,
@@ -522,6 +586,66 @@ mod tests {
assert_eq!(gateway.welcome().effective_tap_mtu(), 972);
}
#[test]
fn removes_client_from_room_indexes() {
let mut registry = RoomRegistry::default();
let client = registry.join(client_hello(1)).unwrap();
registry.join(client_hello(2)).unwrap();
let result = registry.leave(&room(), client.peer().peer_id()).unwrap();
let snapshot = registry.snapshot(&room()).unwrap();
assert_eq!(result.peer(), client.peer());
assert!(!result.room_removed());
assert_eq!(snapshot.clients().len(), 1);
assert!(registry.join(client_hello(1)).is_ok());
}
#[test]
fn removes_empty_room_after_last_peer_leaves() {
let mut registry = RoomRegistry::default();
let client = registry.join(client_hello(1)).unwrap();
let result = registry.leave(&room(), client.peer().peer_id()).unwrap();
assert_eq!(result.peer(), client.peer());
assert!(result.room_removed());
assert_eq!(registry.room_count(), 0);
assert!(registry.snapshot(&room()).is_none());
}
#[test]
fn removes_gateway_without_removing_room_when_clients_remain() {
let mut registry = RoomRegistry::default();
let gateway = registry.join(gateway_hello()).unwrap();
registry.join(client_hello(1)).unwrap();
let result = registry.leave(&room(), gateway.peer().peer_id()).unwrap();
let snapshot = registry.snapshot(&room()).unwrap();
assert_eq!(result.peer(), gateway.peer());
assert!(!result.room_removed());
assert!(snapshot.gateway().is_none());
assert_eq!(snapshot.clients().len(), 1);
assert!(registry.join(gateway_hello()).is_ok());
}
#[test]
fn reports_unknown_peer_on_leave() {
let mut registry = RoomRegistry::default();
registry.join(client_hello(1)).unwrap();
let error = registry.leave(&room(), 99).unwrap_err();
assert_eq!(
error,
ForwardingError::UnknownIngressPeer {
room: room(),
peer_id: 99,
}
);
}
#[test]
fn forwards_unknown_client_unicast_to_gateway() {
let mut registry = RoomRegistry::default();