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:
@@ -24,6 +24,11 @@
|
||||
//! never from `https` to `http`, and the custom
|
||||
//! [`HttpOptions::headers`] are not sent to another origin.
|
||||
//!
|
||||
//! Timeouts scale with the request: [`HttpOptions::timeout`] to connect
|
||||
//! and to get the response headers, and for the body that plus its size at
|
||||
//! [`HttpOptions::min_speed`] — a slow link is not cut off mid-block, a
|
||||
//! stalled connection still is.
|
||||
//!
|
||||
//! Transient failures — connection errors, timeouts, `408`/`429`/`5xx`, and
|
||||
//! a body shorter or longer than its `Content-Range` — are retried with
|
||||
//! exponential backoff. Responses are requested with
|
||||
@@ -53,8 +58,15 @@ pub struct HttpOptions {
|
||||
pub retries: u32,
|
||||
/// Delay before the first retry; doubled for each further one.
|
||||
pub backoff: Duration,
|
||||
/// Timeout of one request, from connecting to the end of the body.
|
||||
/// Time allowed to connect, and then to receive the response headers.
|
||||
/// The body gets this plus the time it takes at
|
||||
/// [`min_speed`](Self::min_speed), so a request's budget grows with its
|
||||
/// size: a slow but moving link is not cut off, a stalled one is.
|
||||
pub timeout: Duration,
|
||||
/// Slowest transfer rate tolerated, in bytes per second: receiving a
|
||||
/// body of `n` bytes may take `timeout + n / min_speed` (16 KiB/s by
|
||||
/// default: 94 s for a 1 MiB block, 9 min for an 8 MiB request).
|
||||
pub min_speed: u64,
|
||||
/// Requests of one `read_ranges` call in flight at once.
|
||||
pub max_parallel: usize,
|
||||
/// Bytes fetched by the first request, from offset 0 (the superblock and
|
||||
@@ -88,7 +100,8 @@ impl Default for HttpOptions {
|
||||
HttpOptions {
|
||||
retries: 3,
|
||||
backoff: Duration::from_millis(200),
|
||||
timeout: Duration::from_secs(60),
|
||||
timeout: Duration::from_secs(30),
|
||||
min_speed: 16 << 10,
|
||||
max_parallel: 8,
|
||||
first_request: crate::cache::DEFAULT_BLOCK_SIZE,
|
||||
allow_full_download: false,
|
||||
@@ -282,7 +295,8 @@ impl HttpStorage {
|
||||
let config = ureq::Agent::config_builder()
|
||||
.http_status_as_error(false)
|
||||
.max_redirects(0)
|
||||
.timeout_global(Some(options.timeout))
|
||||
.timeout_connect(Some(options.timeout))
|
||||
.timeout_recv_response(Some(options.timeout))
|
||||
.build();
|
||||
let mut storage = HttpStorage {
|
||||
agent: ureq::Agent::new_with_config(config),
|
||||
@@ -374,6 +388,18 @@ impl HttpStorage {
|
||||
if let Some((a, b)) = range {
|
||||
req = req.header("Range", format!("bytes={a}-{b}"));
|
||||
}
|
||||
// The body's budget scales with what it may carry: the range, or
|
||||
// a whole file the server may send instead.
|
||||
let mut body = range.map_or(0, |(a, b)| b.saturating_sub(a).saturating_add(1));
|
||||
if self.options.allow_full_download {
|
||||
body = body.max(self.options.max_full_download);
|
||||
}
|
||||
let secs = body as f64 / self.options.min_speed.max(1) as f64;
|
||||
let body_timeout = self
|
||||
.options
|
||||
.timeout
|
||||
.saturating_add(Duration::try_from_secs_f64(secs).unwrap_or(Duration::MAX));
|
||||
let mut req = req.config().timeout_recv_body(Some(body_timeout)).build();
|
||||
match &self.validator {
|
||||
Validator::ETag(e) => req = req.header("If-Match", e),
|
||||
Validator::LastModified(t) => req = req.header("If-Unmodified-Since", t),
|
||||
|
||||
Reference in New Issue
Block a user