Skip to content

Commit 8824fc2

Browse files
maxholmanclaude
andcommitted
fix(peers): reconnecting peer evicts old entry instead of duplicating
When a peer reconnects with the same name, the old connection is now disconnected (Disconnect sent on its control channel) and its registry entry replaced. No more stale indeterminate entries accumulating alongside new connections with different roles. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f23d63a commit 8824fc2

1 file changed

Lines changed: 20 additions & 22 deletions

File tree

crates/core/src/control/peers.rs

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,17 @@ impl Registry {
149149
) -> (String, u64) {
150150
let connection_id = self.next_connection_id.fetch_add(1, Ordering::Relaxed) + 1;
151151

152-
// Disambiguate duplicate names using the unique connection ID.
153-
let peers = self.peers.load();
154-
let peer_id = if peers.contains_key(&id) {
155-
let disambiguated = format!("{id}#{connection_id}");
156-
tracing::info!("Peer name {id:?} already registered, using {disambiguated:?}");
157-
disambiguated
158-
} else {
159-
id.clone()
160-
};
152+
// If a peer with the same name already exists, evict it — the new
153+
// connection supersedes the old one (reconnect scenario). Send a
154+
// Disconnect so the old transport closes cleanly.
155+
if self.peers.load().contains_key(&id) {
156+
tracing::info!("Peer {id:?} reconnected, replacing existing entry");
157+
self.send_disconnect(&id, "superseded by new connection");
158+
// Remove from peers map; the old session task will notice its
159+
// control channel closed and clean up.
160+
self.unregister(&id);
161+
}
162+
let peer_id = id.clone();
161163

162164
let info = PeerInfo {
163165
id: peer_id.clone(),
@@ -503,21 +505,16 @@ mod tests {
503505
);
504506
assert_eq!(id1, "peer1");
505507

508+
// Re-register same name — old entry is evicted, new one takes the name.
506509
let (id2, _) = registry.register(
507510
"peer1".into(),
508511
"1.2.3.4:9999".into(),
509512
NodeRole::Exit,
510513
ConnectionSide::Accept,
511514
);
512-
assert!(
513-
id2.starts_with("peer1#"),
514-
"expected disambiguated id, got {id2}"
515-
);
516-
assert_eq!(registry.count(), 2);
515+
assert_eq!(id2, "peer1", "reconnect should reuse the name");
516+
assert_eq!(registry.count(), 1, "old entry should be evicted");
517517

518-
// Both are independently unregisterable.
519-
registry.unregister(&id1);
520-
assert_eq!(registry.count(), 1);
521518
registry.unregister(&id2);
522519
assert_eq!(registry.count(), 0);
523520
}
@@ -542,19 +539,20 @@ mod tests {
542539
ConnectionSide::Accept,
543540
);
544541
assert_eq!(peer_id1, "peer1");
545-
// Re-register same peer — now disambiguated as "peer1#N".
542+
// Re-register same peer — old entry is evicted, new one takes the name.
546543
let (peer_id2, gen2) = registry.register(
547544
"peer1".into(),
548545
"1.2.3.4:9999".into(),
549546
NodeRole::Exit,
550547
ConnectionSide::Accept,
551548
);
549+
assert_eq!(peer_id2, "peer1");
552550
assert_ne!(gen1, gen2);
553-
assert_eq!(registry.count(), 2);
551+
assert_eq!(registry.count(), 1);
554552

555-
// Old task unregisters with stale connection_id on the original key — should succeed
556-
// since the original "peer1" entry still exists.
557-
assert!(registry.unregister_if_current(&peer_id1, gen1).is_some());
553+
// Old task tries to unregister with stale connection_id — should be a no-op
554+
// since the entry now belongs to the new connection.
555+
assert!(registry.unregister_if_current(&peer_id1, gen1).is_none());
558556
assert_eq!(registry.count(), 1);
559557

560558
// New task unregisters with current connection_id — should succeed.

0 commit comments

Comments
 (0)