A request that joins an in-flight fetch waits in silence for the whole transfer #2
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
When a request arrives for a blob another request is already fetching, it
joins the flight as a follower and calls
Flight::wait(
crates/rustingface-core/src/flight.rs:100), which blocks — with notimeout and no bytes on the wire — until the leader's entire transfer
completes. Only then is the blob read back from the bucket and served.
For a 3.3GB shard at the ~8MB/s a GPU host gets to caveman, that is 400+
seconds before the response headers are even written.
huggingface_hub'sdefault read timeout is 10 seconds (
HF_HUB_DOWNLOAD_TIMEOUT), so theclient gives up roughly forty times over before rustingface says anything.
From the client this is indistinguishable from a hung server. From the
server nothing is wrong at all: the leader's transfer succeeds and logs
stored blob and recorded manifest entry.Why this is worth fixing now
This is the same failure shape as the ranged-fetch half of #1 — the client
holding an open connection receiving nothing while the server considers
itself healthy — and it is the part #1's fix did not address. #1 fixed
the leader path (a ranged request now streams its slice from the tee as
the bytes pass). The follower path still blocks.
It also gets more likely as a result of fixing #1, not less. Lifting the
180s x bandwidthceiling means transfers now legitimately run for manyminutes, which widens the window in which a second request can land on one.
How it is reached
hf download --max-workers Nis enough. Any of:transfer from that first attempt is still running (this is the common
one — the transfer deliberately survives client disconnect, so a prompt
retry lands squarely on it);
fleet-rollout case single-flight exists to serve.
Constraints on any fix
tests/sovereignty.rsasserts it ("a fleet-wide rollout must not become eight concurrent
pulls"). Letting followers fetch upstream independently trades this bug
for the one the design was built to prevent.
passed. They are gone from the leader's stream and not yet readable in
the bucket (the multipart upload is incomplete).
flight.rsis rightthat a follower attached mid-body would get a stream missing its start.
be about to answer": bound the wait, and when it is exceeded, return a
retryable response rather than continue holding a connection that the
client has already given up on.
Suggested direction
Bound the follower's wait well under a client read timeout (a few
seconds), and on expiry answer
503withRetry-Afterinstead ofblocking. The leader keeps transferring, the client backs off and retries,
and the retry is served from the bucket once the blob lands. That
converges, keeps single-flight intact, and is honest about what is
happening.
Worth verifying against the pinned client rather than assuming: the
conformance suite should grow a case asserting that a second concurrent
request for a cold blob gets a prompt retryable answer, and that an
unmodified
huggingface_hubrecovers from it.Measured the pinned client before building on the suggestion above, and it
does not hold up. Correcting it.
huggingface_hub0.36 routes file requests through_request_wrapper->http_backoff, whose defaults are:Two things follow:
Retry-Afteris ignored. The loopsleeps its own exponential backoff (
time.sleep(sleep_time)) and neverreads the header. Five retries at 1, 2, 4, 8, 8 is ~23s of sleeping
plus five round trips.
So for a 3.3GB shard at 8MB/s — a ~420s transfer — a follower answering
503 promptly gives the client roughly 50 seconds of trying before it
raises. It does not converge. The same arithmetic sinks "wait and let the
client time out", since
requests.Timeoutis retried out of the samebudget.
What this means. A prompt 503 is still a strict improvement over
hanging: it converges for everything the leader can finish inside the
client's budget (configs, tokenizers, and shards up to roughly 200MB at
8MB/s), and for anything larger it fails in seconds with a legible reason
instead of hanging invisibly. But it is a mitigation, not a fix, and it
should not be described as one.
Converging for large blobs requires serving the follower bytes, and
every way of doing that runs into something:
keeps a single stored copy, but a fleet rollout becomes N concurrent
upstream pulls, which is the exact harm single-flight exists to prevent
(spec §7, and the sovereignty test asserting it).
requested offset is within the buffer of the leader's current position.
A client resuming from where it was cut is usually far behind that.
prefix while the leader appends — actually converges, but makes a local
file part of the serving path, which cuts against the bucket being the
whole of the system state.
That last one is a real design question rather than a bug fix, so it wants
deciding on its own merits rather than being smuggled in here.
Proposed split: take the prompt-503 mitigation now, and keep this issue
open for the convergence question with the three options above on the
table.