fix(core): per-ticket rejoin bootstrap so room detours don't strand a rejoiner (A8)
Found by manual 2-machine exploration: desktop creates room A, dopedart joins; desktop leaves A, creates room B, leaves B, then rejoins A — and dopedart (still in A) isn't reconnected until dopedart itself leaves and rejoins. Root cause: `known_peers` (the A8 rejoin-bootstrap memory) was a single flat set cleared on ANY join-ticket change. The detour through room B wiped the memory of room A's peers, so rejoining A — whose ticket names the creator itself as host — produced an EMPTY bootstrap (`compute_bootstrap` drops self), leaving the desktop isolated in the gossip topic with no one to dial. dopedart never re-dials a peer that's already a (now-departed) neighbor, so they never relink. Fix: key `known_peers` by room ticket (`HashMap<ticket, HashMap<peer, addr>>`) and stop clearing it. The event loop records peers under its room's ticket; a join pulls bootstrap targets from that ticket's bucket. Revisiting a room after a detour now still remembers its peers. Pre-existing bug (logic unchanged by the B1 refactor); reproduces on pre-B1 main too. 256 lib tests green, clippy --all-targets clean. NOT yet 2-machine field-verified — the exact A→B→A scenario is the gate (dopedart is up; verifying next). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+46
-27
@@ -535,15 +535,16 @@ async fn run_core_loop(
|
||||
error: identity_error.clone(),
|
||||
})
|
||||
.await;
|
||||
// Peers seen in the current/most-recent room, retained ACROSS leave so a
|
||||
// rejoin can bootstrap to them. This is the fix for A8: the room creator's own
|
||||
// ticket lists only themselves as host, so on rejoin these retained peers are
|
||||
// their only dial targets. Reset when the joined ticket changes (different /
|
||||
// brand-new room — see the Join handler). Keyed by id so updates refresh addr.
|
||||
let known_peers: Arc<std::sync::Mutex<HashMap<EndpointId, EndpointAddr>>> =
|
||||
// Peers seen per room, keyed by the room's join ticket, retained ACROSS leaves
|
||||
// so a rejoin can bootstrap to them. This is the fix for A8: the room creator's
|
||||
// own ticket lists only themselves as host, so on rejoin these retained peers
|
||||
// are their only dial targets. Keyed by TICKET (not one flat set) so that
|
||||
// revisiting a room after a detour through another one still remembers the
|
||||
// first room's peers — the old single-set version cleared them on any ticket
|
||||
// change, so an A→B→A bounce stranded the rejoiner with an empty bootstrap.
|
||||
// Inner map keyed by peer id so updates refresh the address.
|
||||
let known_peers: Arc<std::sync::Mutex<HashMap<String, HashMap<EndpointId, EndpointAddr>>>> =
|
||||
Arc::new(std::sync::Mutex::new(HashMap::new()));
|
||||
// The ticket of the room we last (re)joined, to detect room changes above.
|
||||
let mut last_join_ticket: Option<String> = None;
|
||||
|
||||
let audio_backend = Arc::new(PipeWireBackend::new());
|
||||
|
||||
@@ -687,18 +688,16 @@ async fn run_core_loop(
|
||||
avatar: current_avatar.clone(),
|
||||
};
|
||||
|
||||
// Retain peers across leave so a rejoin can dial them (A8). A
|
||||
// different ticket (or a fresh "create", which mints a random
|
||||
// ticket) shares no peers with the last room, so reset; rejoining
|
||||
// the SAME ticket keeps the set — that's what gives the room
|
||||
// creator a dial target on rejoin. Then snapshot it as the extra
|
||||
// bootstrap set (resolution rides the persistent address book).
|
||||
if last_join_ticket.as_deref() != Some(ticket_str.as_str()) {
|
||||
known_peers.lock().unwrap().clear();
|
||||
}
|
||||
last_join_ticket = Some(ticket_str.clone());
|
||||
let extra_bootstrap: Vec<EndpointAddr> =
|
||||
known_peers.lock().unwrap().values().cloned().collect();
|
||||
// Snapshot THIS room's retained peers (by ticket) as extra bootstrap
|
||||
// targets so a rejoin can dial them (A8) — including after a detour
|
||||
// through another room, since the per-ticket archive isn't cleared.
|
||||
// Resolution rides the persistent address book.
|
||||
let extra_bootstrap: Vec<EndpointAddr> = known_peers
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&ticket_str)
|
||||
.map(|peers| peers.values().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
crate::log_msg(&format!("Attempting room_state.join with self_state={:?}, extra_bootstrap={:?}", self_state, extra_bootstrap.iter().map(|a| a.id).collect::<Vec<_>>()));
|
||||
if let Err(e) = room_state.join(&ticket_str, self_state.clone(), extra_bootstrap).await {
|
||||
@@ -1047,6 +1046,9 @@ async fn run_core_loop(
|
||||
let multitrack_events = multitrack.clone();
|
||||
let is_multitrack_events = is_multitrack.clone();
|
||||
let known_peers_events = known_peers.clone();
|
||||
// The ticket of the room this event loop serves, so peer add/remove
|
||||
// updates the right per-ticket bucket in `known_peers` (A8 archive).
|
||||
let ticket_events = ticket_str.clone();
|
||||
let event_task = tokio::spawn(async move {
|
||||
while let Some(event) = room_events.recv().await {
|
||||
match event {
|
||||
@@ -1059,8 +1061,14 @@ async fn run_core_loop(
|
||||
// Hand over the full address so reconnects can dial
|
||||
// it directly rather than via the gossip lookup.
|
||||
transport_events.connect_peer(state.addr.clone()).await;
|
||||
// Retain this peer as a future rejoin bootstrap target (A8).
|
||||
known_peers_events.lock().unwrap().insert(peer_id, state.addr.clone());
|
||||
// Retain this peer under this room's ticket as a
|
||||
// future rejoin bootstrap target (A8).
|
||||
known_peers_events
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(ticket_events.clone())
|
||||
.or_default()
|
||||
.insert(peer_id, state.addr.clone());
|
||||
// If a multitrack recording is live, give this peer
|
||||
// its own stem track (silence-padded back to t=0).
|
||||
if is_multitrack_events.load(Ordering::Relaxed)
|
||||
@@ -1076,9 +1084,14 @@ async fn run_core_loop(
|
||||
cancel_grace_timer(&grace_timers_events, &peer_id);
|
||||
seen_connected_events.lock().unwrap().remove(&peer_id);
|
||||
// Graceful leave: drop them as a rejoin dial target
|
||||
// (a transient PeerConnectionLost deliberately does
|
||||
// NOT, so we can still re-dial a peer who's still up).
|
||||
known_peers_events.lock().unwrap().remove(&peer_id);
|
||||
// for this room (a transient PeerConnectionLost
|
||||
// deliberately does NOT, so we can still re-dial a
|
||||
// peer who's still up).
|
||||
if let Some(peers) =
|
||||
known_peers_events.lock().unwrap().get_mut(&ticket_events)
|
||||
{
|
||||
peers.remove(&peer_id);
|
||||
}
|
||||
transport_events.disconnect_peer(peer_id).await;
|
||||
jitter_events.lock().await.remove(&peer_id);
|
||||
let _ = ui_tx_events.send(UiEvent::PeerLeft { id: peer_id }).await;
|
||||
@@ -1092,8 +1105,14 @@ async fn run_core_loop(
|
||||
// re-records the same address.
|
||||
cancel_grace_timer(&grace_timers_events, &peer_id);
|
||||
transport_events.connect_peer(state.addr.clone()).await;
|
||||
// Refresh the retained rejoin target with the fresh addr (A8).
|
||||
known_peers_events.lock().unwrap().insert(peer_id, state.addr.clone());
|
||||
// Refresh this room's retained rejoin target with the
|
||||
// fresh addr (A8).
|
||||
known_peers_events
|
||||
.lock()
|
||||
.unwrap()
|
||||
.entry(ticket_events.clone())
|
||||
.or_default()
|
||||
.insert(peer_id, state.addr.clone());
|
||||
let _ = ui_tx_events.send(UiEvent::PeerUpdated { id: peer_id, state }).await;
|
||||
}
|
||||
RoomEvent::ChatMessage { from, name, text, .. } => {
|
||||
|
||||
Reference in New Issue
Block a user