aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-05 04:11:59 +0100
committertv <tv@krebsco.de>2026-11-05 04:11:59 +0100
commit0d3e68e20f0812a2fdf5c9736cf3161f30912211 (patch)
tree57a194e1b060eb6c21e916639d3cd4c08aa33c7b
parent275772f6ecd58eefdbefa0b090146203bf62cb66 (diff)
chunk timeout adapts
-rw-r--r--src/swarm/blob.rs80
1 files changed, 74 insertions, 6 deletions
diff --git a/src/swarm/blob.rs b/src/swarm/blob.rs
index 6efa5ca..c2b397c 100644
--- a/src/swarm/blob.rs
+++ b/src/swarm/blob.rs
@@ -21,7 +21,8 @@
//! chunks, a few in flight per peer at first, more as they come (up to
//! [`MAX_WINDOW`]), from up to [`MAX_SOURCES`] peers. Every chunk is
//! checked against the manifest; a peer that sends a bad one is dropped, a chunk
-//! that doesn't arrive in time is asked of another peer. The caller checks the whole
+//! that doesn't arrive in time (three times as long as its chunks took lately, where
+//! another peer could send it) is asked of another peer. The caller checks the whole
//! (for a change: its hash).
//!
//! The chunk size is the serving node's (`--blob-chunk`); the manifest carries it.
@@ -68,8 +69,10 @@ const MANIFEST_TIMEOUT: Duration = Duration::from_secs(15);
/// Peers asked for the manifest at once.
const MANIFESTS_AT_ONCE: usize = 3;
/// How long to wait for a chunk before asking another peer (raised by the peer's
-/// round-trip time).
+/// round-trip time), until chunks from it show how long they take: then three times as
+/// long as they took lately (`Source::timeout_now`), but at least `CHUNK_TIMEOUT_MIN`.
const CHUNK_TIMEOUT: Duration = Duration::from_secs(60);
+const CHUNK_TIMEOUT_MIN: Duration = Duration::from_secs(5);
/// A download gives up after this long without any chunk arriving.
const STALL_TIMEOUT: Duration = Duration::from_secs(180);
/// Bad chunks after which a peer is no longer asked.
@@ -410,7 +413,34 @@ struct Source {
in_flight: HashMap<u32, Instant>,
window: usize,
bad: u32,
+ /// The longest a chunk is waited for (`CHUNK_TIMEOUT`, raised by the round trip).
timeout: Duration,
+ rtt: Duration,
+ /// How long its chunks took lately, from the request (smoothed), once one came.
+ took: Option<Duration>,
+}
+
+impl Source {
+ /// How long a chunk from it is waited for now: three times as long as its chunks
+ /// took lately (they wait in its queue too, as long as the window is), within
+ /// `CHUNK_TIMEOUT_MIN` (and two round trips) and `timeout`. So a source that stops
+ /// sending hands its chunks on within seconds where they came quickly, and a slow
+ /// link isn't given up on for being slow. Only where another peer could take them
+ /// (`fetch`).
+ fn timeout_now(&self) -> Duration {
+ match self.took {
+ None => self.timeout,
+ Some(took) => (took * 3).clamp(CHUNK_TIMEOUT_MIN + self.rtt * 2, self.timeout),
+ }
+ }
+
+ /// A chunk asked for `took` ago came.
+ fn came(&mut self, took: Duration) {
+ self.took = Some(match self.took {
+ None => took,
+ Some(before) => (before * 7 + took) / 8,
+ });
+ }
}
/// Downloads blob `id` from `peers` (all thought to hold it): of `expected_size` bytes
@@ -525,14 +555,15 @@ pub(crate) async fn fetch(
let Some(peer) = candidates.pop_front() else {
break;
};
- let timeout =
- CHUNK_TIMEOUT + rtts.get(&peer).copied().flatten().unwrap_or_default() * 4;
+ let rtt = rtts.get(&peer).copied().flatten().unwrap_or_default();
sources.push(Source {
peer,
in_flight: HashMap::new(),
window: WINDOW,
bad: 0,
- timeout,
+ timeout: CHUNK_TIMEOUT + rtt * 4,
+ rtt,
+ took: None,
});
}
if sources.is_empty() {
@@ -588,6 +619,7 @@ pub(crate) async fn fetch(
}
if let Some(sent_at) = sent_at {
debug!(peer = %from, blob = %id, index, ms = sent_at.elapsed().as_millis() as u64, "Blob chunk");
+ source.came(sent_at.elapsed());
}
chunks[slot] = Some(data);
done += 1;
@@ -606,8 +638,15 @@ pub(crate) async fn fetch(
// Chunks that took too long go to another peer; the slow peer gets a smaller
// window. Peers that sent bad chunks are dropped.
let now = Instant::now();
+ // The quick timeout only where another peer could take the chunks: with one
+ // source, asking it again would only bring them twice over a slow link.
+ let others = sources.len() > 1 || !candidates.is_empty();
for source in &mut sources {
- let timeout = source.timeout;
+ let timeout = if others {
+ source.timeout_now()
+ } else {
+ source.timeout
+ };
let before = source.in_flight.len();
source
.in_flight
@@ -690,6 +729,35 @@ pub(crate) async fn fetch_delta(
mod tests {
use super::*;
+ /// A source's chunk timeout follows how long its chunks took, within bounds: the
+ /// long one until a chunk came.
+ #[test]
+ fn chunk_timeouts_follow_the_source() {
+ let mut source = Source {
+ peer: "p".into(),
+ in_flight: HashMap::new(),
+ window: WINDOW,
+ bad: 0,
+ timeout: CHUNK_TIMEOUT,
+ rtt: Duration::from_millis(100),
+ took: None,
+ };
+ assert_eq!(source.timeout_now(), CHUNK_TIMEOUT);
+ source.came(Duration::from_secs(4));
+ assert_eq!(source.timeout_now(), Duration::from_secs(12));
+ source.came(Duration::from_millis(100));
+ assert!(source.timeout_now() < Duration::from_secs(12));
+ for _ in 0..50 {
+ source.came(Duration::from_millis(100));
+ }
+ assert_eq!(
+ source.timeout_now(),
+ CHUNK_TIMEOUT_MIN + Duration::from_millis(200)
+ );
+ source.came(Duration::from_secs(600));
+ assert_eq!(source.timeout_now(), CHUNK_TIMEOUT, "at most the long one");
+ }
+
/// One delta at a time: offers meanwhile wait, and are named when it ends.
#[test]
fn one_delta_at_a_time() {