fix: keep reconnecting across a long outage instead of evicting on NeighborDown
Field test of the long-outage path failed: ~31s into a Wi-Fi drop the peer
vanished with no "Reconnecting..." indicator and never came back. Logs showed
the chain `Gossip NeighborDown -> Removed peer -> Transport: stopped
supervising peer`.
Root cause: a transient gossip NeighborDown was routed to RoomEvent::PeerLeft,
same as a graceful leave, so core called disconnect_peer -> supervisor.abort().
That aborted the very reconnect supervisor that was meant to redial -- before
its own reconnect loop (which re-emits Connecting and re-dials the retained
09acefd address with backoff) ever ran. The supervisor + retained-address fix
were effectively dead code in the field, which is also why the loopback tests
(they drive the supervisor directly) never caught it.
Fix: decouple a transient drop from a graceful leave.
- gossip.rs: NeighborDown now emits the new RoomEvent::PeerConnectionLost
instead of PeerLeft. A graceful GossipMessage::Leave still emits PeerLeft.
- core: on PeerConnectionLost, do NOT disconnect the peer. Keep its supervisor
alive (it redials the retained address and drives the yellow indicator) and
arm a per-peer reconnect grace timer (RECONNECT_GRACE = 45s, comfortably past
the ~30s QUIC idle timeout). The peer is evicted only if the link hasn't
recovered when the timer fires. A gossip rejoin (PeerJoined/PeerUpdated) or a
transport reconnect (ConnEvent::Connected) cancels the timer first; session
shutdown aborts all pending timers so none fire a stray eviction.
Builds clean, clippy clean, 3 transport tests pass. NOT yet field-verified --
re-test the long-outage path on a real call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -193,10 +193,17 @@ impl RoomState for IrohGossipState {
|
||||
}
|
||||
Ok(iroh_gossip::api::Event::NeighborDown(peer_id)) => {
|
||||
crate::log_msg(&format!("Gossip event: NeighborDown={:?}", peer_id));
|
||||
// A NeighborDown is a *transient* loss, not a graceful
|
||||
// leave: emit PeerConnectionLost so the core marks the peer
|
||||
// "reconnecting" and keeps its audio supervisor redialing,
|
||||
// rather than tearing everything down. (Treating this as a
|
||||
// PeerLeft is exactly what defeated reconnect in the field —
|
||||
// it aborted the supervisor ~30s in.) We still drop our
|
||||
// cached presence entry; a rejoin re-announces as new.
|
||||
let removed = peers.lock().unwrap().remove(&peer_id).is_some();
|
||||
if removed {
|
||||
crate::log_msg(&format!("Removed peer due to NeighborDown: {:?}", peer_id));
|
||||
let _ = event_tx.send(RoomEvent::PeerLeft(peer_id)).await;
|
||||
crate::log_msg(&format!("Peer connection lost (NeighborDown): {:?}", peer_id));
|
||||
let _ = event_tx.send(RoomEvent::PeerConnectionLost(peer_id)).await;
|
||||
}
|
||||
}
|
||||
Ok(other) => {
|
||||
|
||||
Reference in New Issue
Block a user