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:
@@ -46,6 +46,7 @@ Public relay binary and relay-owned room state:
|
||||
- one gateway per room, duplicate client MAC rejection, and room limits
|
||||
- stable effective room MTU chosen before Ethernet datagrams flow
|
||||
- Ethernet datagram forwarding decisions with no ingress reflection
|
||||
- peer leave cleanup for room membership and MAC indexes
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user