clawhdf5-remote: request timeouts scale with the body

timeout_global (60 s) covered a whole request, and a request can carry
8 MiB (max_request): below about 140 KB/s every block run timed out, was
retried from scratch and failed, so a slow link could not read remote
files at all.

HttpOptions::timeout (now 30 s) bounds connecting and receiving the
response headers; the body gets timeout + its size at the new
HttpOptions::min_speed (16 KiB/s by default: 94 s for a 1 MiB block).
A slow but moving link is not cut off; a stalled one still fails.
(ureq has no idle timeout; its body timeout is a total budget.)

The test server can throttle bodies and stall mid-body. Test: a 256 KiB
block at 256 KiB/s reads with a 300 ms timeout (it failed before), and a
body stalled for 20 s fails in under 5 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 18:34:31 -05:00
co-authored by Claude Opus 5.5
parent 61e34927dc
commit 30a1ed6b9c
3 changed files with 91 additions and 7 deletions
+28 -4
View File
@@ -55,6 +55,11 @@ pub struct Shared {
/// When non-zero, answer every request for a served file with this
/// status (and an empty body).
pub force_status: AtomicU32,
/// When non-zero, send bodies at about this many bytes per second.
pub throttle_bps: AtomicU64,
/// When non-zero, stop this many milliseconds after the headers and
/// half the body (a stalled connection).
pub stall_ms: AtomicU64,
/// Answer ranges with a `Content-Range` one byte off.
pub wrong_range: AtomicBool,
/// Path → `Location`: answered `302 Found` (counted as a request).
@@ -384,13 +389,32 @@ fn serve(conn: TcpStream, s: &Shared) -> std::io::Result<()> {
} else {
body
};
let mut response = head.into_bytes();
response.extend_from_slice(body);
// Counted before the client can have the bytes, so a test that
// resets the counters after a read never sees them arrive late.
s.bytes.fetch_add(body.len() as u64, Ordering::SeqCst);
out.write_all(&response)?;
out.flush()?;
let bps = s.throttle_bps.load(Ordering::SeqCst);
let stall = s.stall_ms.load(Ordering::SeqCst);
if bps > 0 || stall > 0 {
out.write_all(head.as_bytes())?;
let (first, rest) = body.split_at(if stall > 0 { body.len() / 2 } else { 0 });
out.write_all(first)?;
out.flush()?;
if stall > 0 {
std::thread::sleep(Duration::from_millis(stall));
}
for piece in rest.chunks(4096) {
out.write_all(piece)?;
out.flush()?;
if bps > 0 {
std::thread::sleep(Duration::from_micros(4096 * 1_000_000 / bps));
}
}
} else {
let mut response = head.into_bytes();
response.extend_from_slice(body);
out.write_all(&response)?;
out.flush()?;
}
if truncate || close {
let _ = out.shutdown(std::net::Shutdown::Both);
return Ok(());