A total-duration request_timeout makes large-blob fetches fail deterministically in proxy mode #1
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
With
blob_delivery = "proxy", a blob transfer is bounded byrequest_timeout(300s here). Since a streaming download's duration issize / bandwidth, this turns a total-duration cap into a maximumdownloadable file size — and above that size, no number of retries can
ever succeed.
Fetching
Qwen/Qwen3.8-Flash-Next(360 GB, 131 shards of ~3.3 GB) fromrf.internalstalls permanently at 52 of 144 blobs.Evidence
Client is
hf downloadon a fleet host,HF_ENDPOINT=https://rf.internal.Measured bandwidth from that host, on a blob rustingface has already
stored (so no upstream pull-through in the path):
The mesh link is fine; the ~8 MB/s is the S3 backend
(
caveman.kosherinata.internal:9000, a different site) streamingthrough the service.
The partial files match
request_timeout x per-connection bandwidthalmost exactly. With two concurrent workers sharing ~8 MB/s, each gets
~4–4.8 MB/s, and 300s at that rate is 1.2–1.4 GB. The three
.incompletefiles were:That is the cap cutting the transfer mid-stream, not a stall — and it
implies even
--max-workers 1fails: 300s at the full 8 MB/s is~2.4 GB, still short of a 3.3 GB shard.
Across 20 attempts: 21
httpx.ReadTimeout, 2httpx.RemoteProtocolError, and the.incompletefiles stoppedgrowing entirely (mtimes went stale while attempts continued), so the
client is not resuming them across attempts either. The retry loop
cannot converge.
Why I think the cap is the wrong control
client_stall_timeout = 60salready handles the case a total-durationtimeout is usually reaching for — a peer that has gone away, or a
transfer making no progress. It is an idle/rate guard, which is the
right shape for a stream.
request_timeouton top of it adds a second, different rule: thistransfer must finish within 300s regardless of how healthy it is. For a
model registry, that makes success a function of file size × bandwidth —
so the same client, same file, succeeds on a fast link and fails forever
on a slow one, with no diagnostic difference between them.
Also: the failure is invisible server-side
While the client was accumulating 21
ReadTimeouts,journalctl -u rustingfacefor the same window logged no error, warning, 502 or 504 —only a successful
stored blob and recorded manifest entry. Whatevercuts the transfer does not say so. Operationally this reads as "the
client is flaky", which is where I started.
Suggestions, in the order I would take them
request_timeout, leavingclient_stall_timeoutas the guard. A transfer that is still movingbytes is not a hung request.
blob_deliverytoredirectfor blobs over some size,or generally. The 302-to-presigned-URL path takes the service out of
the byte path entirely, which is what a 360 GB repo wants, and it is
already implemented — it is just not the default. (Needs clients to
reach the S3 endpoint directly, which is a deployment question, not a
code one.)
path, bytes sent and which timeout fired. Right now the server is
silent about its own cut.
Workaround for anyone hitting this
Ranged GETs sized to stay under the cap work fine — the 32 MB probes
above are from the same host, same file, same session. So a client that
chunks below
request_timeout x bandwidthcompletes;hf downloaddoesnot chunk, so it cannot.