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:
@@ -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(());
|
||||
|
||||
@@ -800,3 +800,37 @@ fn a_200_covering_the_requested_range_is_the_whole_file() {
|
||||
let e = HttpStorage::open(&server.url("/t.h5"), opts).unwrap_err();
|
||||
assert!(matches!(e, RemoteError::RangeNotSupported(_)), "{e}");
|
||||
}
|
||||
|
||||
/// A slow link is not cut off: the body's time budget grows with its size
|
||||
/// (`timeout` + size at `min_speed`), so a 256 KiB block at 256 KiB/s
|
||||
/// (1 s) reads with a 300 ms `timeout`. A stalled body still fails, soon.
|
||||
#[test]
|
||||
fn slow_links_read_and_stalled_ones_fail() {
|
||||
let bytes = multi_block_file();
|
||||
let server = Server::start(vec![("/m.h5".into(), bytes.clone())]);
|
||||
let url = server.url("/m.h5");
|
||||
let mut opts = quick();
|
||||
opts.cache.block_size = 256 << 10;
|
||||
opts.cache.max_request = 256 << 10;
|
||||
opts.http.timeout = Duration::from_millis(300);
|
||||
server
|
||||
.shared
|
||||
.throttle_bps
|
||||
.store(256 << 10, Ordering::SeqCst);
|
||||
let f = open_url_with(&url, &opts).unwrap();
|
||||
assert_eq!(f.root().groups().unwrap(), ["grp"]);
|
||||
assert_eq!(
|
||||
f.dataset("grp/small").unwrap().read_f64().unwrap(),
|
||||
[1.0, 2.0, 3.0]
|
||||
);
|
||||
server.shared.throttle_bps.store(0, Ordering::SeqCst);
|
||||
|
||||
// Stalled mid-body for 20 s: a timeout well before that.
|
||||
server.shared.stall_ms.store(20_000, Ordering::SeqCst);
|
||||
opts.http.retries = 0;
|
||||
opts.http.min_speed = 64 << 20;
|
||||
let t = std::time::Instant::now();
|
||||
let e = open_url_with(&url, &opts).unwrap_err();
|
||||
assert!(matches!(e, Error::Remote(RemoteError::Transport(_))), "{e}");
|
||||
assert!(t.elapsed() < Duration::from_secs(5), "{:?}", t.elapsed());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user