Skip to content

Commit dcbf4bf

Browse files
committed
Isolate slow peer connection opens
1 parent 475807e commit dcbf4bf

2 files changed

Lines changed: 169 additions & 1 deletion

File tree

now.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,22 @@ the operator to run `fabric service install`. This change makes that exact
105105
transition green. A present companion binary with a missing service remains a
106106
problem. An enabled service with a missing binary now asks for a matched pair.
107107

108+
PR #171 merged the transition doctor fix as `475807e`. It passed the Nix,
109+
macOS, and deterministic checks. It is not released or deployed.
110+
111+
A retained hetz-to-Silber window measured 300 direct probes over 358.498
112+
seconds. All probes passed. External p50 was 45.013 ms, p95 was 2.117 seconds,
113+
and p99 was 2.675 seconds. Nineteen probes exceeded two seconds. Eighteen of
114+
those probes aligned with the daemon's 20-second peer health loop.
115+
116+
The synchronized internal snapshot recorded 18 Silber probes. All were below
117+
200 ms, and it recorded no transport loss or retry. This instrument is blind
118+
to the external delay. The health loop probes peers in sequence, and
119+
`PeerConnections::get_or_open` holds the full connection-map lock across a
120+
peer dial and generation exchange. A deterministic red test proved that one
121+
slow peer open blocks an existing healthy peer. Branch
122+
`fix/peer-lock-head-of-line` narrows this serialization to one peer.
123+
108124
The local library suite passed 513 tests with five measurements ignored. The
109125
binary suite passed 19 tests, and the update contract passed eight tests. The
110126
five-second latency property measured 502 records, seven scans, a 10.001166 ms

src/mux.rs

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use std::{
2424
collections::{HashMap, HashSet},
2525
fmt,
26+
sync::Arc,
2627
time::{Duration, Instant},
2728
};
2829

@@ -341,6 +342,9 @@ struct LegacyFallback {
341342
pub struct PeerConnections {
342343
local_id: EndpointId,
343344
conns: Mutex<HashMap<EndpointId, PeerConn>>,
345+
/// Serializes connection replacement for one peer without making a slow
346+
/// dial block every other peer in the map.
347+
peer_gates: Mutex<HashMap<EndpointId, Arc<Mutex<()>>>>,
344348
legacy_notices: Mutex<HashSet<(EndpointId, u64)>>,
345349
legacy_fallbacks: Mutex<HashMap<(EndpointId, u64), LegacyFallback>>,
346350
opened_tx: mpsc::UnboundedSender<Connection>,
@@ -353,6 +357,7 @@ impl PeerConnections {
353357
Self {
354358
local_id,
355359
conns: Mutex::new(HashMap::new()),
360+
peer_gates: Mutex::new(HashMap::new()),
356361
legacy_notices: Mutex::new(HashSet::new()),
357362
legacy_fallbacks: Mutex::new(HashMap::new()),
358363
opened_tx,
@@ -361,6 +366,15 @@ impl PeerConnections {
361366
}
362367
}
363368

369+
async fn peer_gate(&self, peer: EndpointId) -> Arc<Mutex<()>> {
370+
self.peer_gates
371+
.lock()
372+
.await
373+
.entry(peer)
374+
.or_insert_with(|| Arc::new(Mutex::new(())))
375+
.clone()
376+
}
377+
364378
/// Open a mux stream to `peer_addr`'s exposure `protocol`, reusing the peer's
365379
/// shared connection (opening it if needed and replacing a dead cached one).
366380
/// A duplicate close gets a short bounded convergence window because a peer
@@ -628,6 +642,8 @@ impl PeerConnections {
628642
protocol: &str,
629643
_activity: StreamActivity,
630644
) -> Result<Option<Connection>> {
645+
let peer_gate = self.peer_gate(peer_addr.id).await;
646+
let _peer_guard = peer_gate.lock().await;
631647
let mut conns = self.conns.lock().await;
632648
if let Some(existing) = conns.get(&peer_addr.id)
633649
&& existing.generation == generation
@@ -652,6 +668,7 @@ impl PeerConnections {
652668
// retain the old canonical connection and reject the replacement.
653669
stale.connection.close(0u32.into(), STALE_GENERATION_REASON);
654670
}
671+
drop(conns);
655672
if self.mux_probe_deferred(peer_addr.id, generation).await {
656673
return Ok(None);
657674
}
@@ -671,6 +688,7 @@ impl PeerConnections {
671688
}
672689
};
673690
let remote_generation = Self::exchange_generations(&connection, generation).await?;
691+
let mut conns = self.conns.lock().await;
674692
conns.insert(
675693
peer_addr.id,
676694
PeerConn {
@@ -696,6 +714,8 @@ impl PeerConnections {
696714
stable_id: usize,
697715
reason: &'static [u8],
698716
) {
717+
let peer_gate = self.peer_gate(peer).await;
718+
let _peer_guard = peer_gate.lock().await;
699719
let mut connections = self.conns.lock().await;
700720
if connections
701721
.get(&peer)
@@ -710,6 +730,8 @@ impl PeerConnections {
710730

711731
/// Make one peer use a fresh multipath connection on its next stream.
712732
pub async fn redial(&self, peer: EndpointId, reason: &[u8]) -> bool {
733+
let peer_gate = self.peer_gate(peer).await;
734+
let _peer_guard = peer_gate.lock().await;
713735
let removed = self.conns.lock().await.remove(&peer);
714736
if let Some(removed) = removed {
715737
removed.connection.close(0u32.into(), reason);
@@ -726,6 +748,8 @@ impl PeerConnections {
726748
remote_generation: u64,
727749
) {
728750
let peer = connection.remote_id();
751+
let peer_gate = self.peer_gate(peer).await;
752+
let _peer_guard = peer_gate.lock().await;
729753
let mut conns = self.conns.lock().await;
730754
let replace = match conns.get(&peer) {
731755
None => true,
@@ -866,7 +890,6 @@ mod tests {
866890
endpoint::presets,
867891
protocol::{AcceptError, ProtocolHandler, Router},
868892
};
869-
use std::sync::Arc;
870893
use std::sync::atomic::{AtomicUsize, Ordering};
871894

872895
const LEGACY_ECHO_ALPN: &[u8] = b"fabric/test-legacy-echo/1";
@@ -1004,6 +1027,43 @@ mod tests {
10041027
}
10051028
}
10061029

1030+
#[derive(Debug, Clone)]
1031+
struct BlockedGenerationMuxEcho {
1032+
entered: Arc<tokio::sync::Barrier>,
1033+
release: Arc<tokio::sync::Notify>,
1034+
}
1035+
1036+
impl ProtocolHandler for BlockedGenerationMuxEcho {
1037+
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
1038+
let result: Result<()> = async {
1039+
let (mut send, mut recv) = connection.accept_bi().await?;
1040+
let _remote_generation = recv.read_u64().await?;
1041+
self.entered.wait().await;
1042+
self.release.notified().await;
1043+
send.write_u64(0).await?;
1044+
send.finish()?;
1045+
1046+
loop {
1047+
let (mut send, mut recv) = match connection.accept_bi().await {
1048+
Ok(pair) => pair,
1049+
Err(_) => break,
1050+
};
1051+
tokio::spawn(async move {
1052+
if MuxStreamHeader::read(&mut recv).await.is_ok() {
1053+
let _ = write_ready(&mut send).await;
1054+
let _ = tokio::io::copy(&mut recv, &mut send).await;
1055+
let _ = send.finish();
1056+
}
1057+
});
1058+
}
1059+
Ok(())
1060+
}
1061+
.await;
1062+
result
1063+
.map_err(|error| AcceptError::from_err(std::io::Error::other(format!("{error:#}"))))
1064+
}
1065+
}
1066+
10071067
async fn finish_streams_then_echo(connection: Connection, failures: usize) {
10081068
let mut streams = 0usize;
10091069
loop {
@@ -1027,6 +1087,98 @@ mod tests {
10271087
}
10281088
}
10291089

1090+
/// A slow connection to one peer must not delay an existing connection to
1091+
/// another peer. Production showed a 2-second delay every 20 seconds when
1092+
/// the health loop probed an absent peer beside a healthy peer.
1093+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1094+
async fn one_slow_peer_open_does_not_block_another_peer() -> Result<()> {
1095+
let client = Endpoint::builder(presets::N0).bind().await?;
1096+
let (opened_tx, _opened_rx) = mpsc::unbounded_channel();
1097+
let manager = Arc::new(PeerConnections::new(client.id(), opened_tx));
1098+
1099+
let healthy = Router::builder(
1100+
Endpoint::builder(presets::N0)
1101+
.alpns(vec![MUX_ALPN.to_vec()])
1102+
.bind()
1103+
.await?,
1104+
)
1105+
.accept(
1106+
MUX_ALPN,
1107+
MuxEcho {
1108+
connections: Arc::new(AtomicUsize::new(0)),
1109+
headers: Arc::new(Mutex::new(Vec::new())),
1110+
},
1111+
)
1112+
.spawn();
1113+
let entered = Arc::new(tokio::sync::Barrier::new(2));
1114+
let release = Arc::new(tokio::sync::Notify::new());
1115+
let slow = Router::builder(
1116+
Endpoint::builder(presets::N0)
1117+
.alpns(vec![MUX_ALPN.to_vec()])
1118+
.bind()
1119+
.await?,
1120+
)
1121+
.accept(
1122+
MUX_ALPN,
1123+
BlockedGenerationMuxEcho {
1124+
entered: entered.clone(),
1125+
release: release.clone(),
1126+
},
1127+
)
1128+
.spawn();
1129+
healthy.endpoint().online().await;
1130+
slow.endpoint().online().await;
1131+
1132+
let first = manager
1133+
.open_mux_stream(
1134+
&client,
1135+
0,
1136+
&healthy.endpoint().addr(),
1137+
"first",
1138+
StreamActivity::Probe,
1139+
)
1140+
.await?;
1141+
drop(first);
1142+
1143+
let slow_manager = manager.clone();
1144+
let slow_client = client.clone();
1145+
let slow_addr = slow.endpoint().addr();
1146+
let slow_open = tokio::spawn(async move {
1147+
slow_manager
1148+
.open_mux_stream(&slow_client, 0, &slow_addr, "slow", StreamActivity::Probe)
1149+
.await
1150+
});
1151+
tokio::time::timeout(Duration::from_secs(3), entered.wait())
1152+
.await
1153+
.context("the slow peer did not reach generation exchange")?;
1154+
1155+
let healthy_open = tokio::time::timeout(
1156+
Duration::from_millis(250),
1157+
manager.open_mux_stream(
1158+
&client,
1159+
0,
1160+
&healthy.endpoint().addr(),
1161+
"healthy",
1162+
StreamActivity::Probe,
1163+
),
1164+
)
1165+
.await;
1166+
release.notify_one();
1167+
let slow_stream = slow_open.await??;
1168+
drop(slow_stream);
1169+
1170+
assert!(
1171+
healthy_open.is_ok(),
1172+
"a slow open to one peer blocked an existing connection to another peer"
1173+
);
1174+
healthy_open.expect("checked timeout")?;
1175+
1176+
slow.shutdown().await?;
1177+
healthy.shutdown().await?;
1178+
client.close().await;
1179+
Ok(())
1180+
}
1181+
10301182
async fn assert_stream_failure_recovery(
10311183
failures: usize,
10321184
expect_same_connection: bool,

0 commit comments

Comments
 (0)