fix(core): a cold range that stops short of the end no longer stalls
Since #1 a ranged GET on a file the bucket does not hold streams its slice from the tee instead of waiting for the whole transfer. For a range that runs to the end of the file -- what a resumed `hf download` sends -- that is exactly right, and it stays. For a range that stops short of the end it is not enough. The slice arrives quickly and then the *last chunk* is withheld until the manifest write lands, which waits on every remaining byte, because the digest is verified only once the whole file has passed. Measured against the live service, asking for the first 1MiB of a 3.3GB shard: http=206 ttfb=1.040851s total=120.001638s bytes=1048169 1,048,169 of 1,048,576 bytes in about a second, then an idle connection for the rest of the transfer. The client's read timeout ends it long before, so in practice the request fails anyway -- slowly and with no explanation. The holdback is not the thing to change: releasing that tail early would signal "durable and recorded" over bytes that are neither, which is precisely what it exists to prevent. So answer 503 with Retry-After instead, and let the fetch run on detached; the retry is served from the bucket. `hf_transfer` splits every download into bounded ranged chunks, so this shape is not hypothetical wherever it is enabled. The existing ranged test now asserts the two-step behaviour and keeps its guarantee that the bucket holds the whole file and never a fragment. A new test pins that a resume to EOF is still served from the tee, so the fix for #1 cannot be undone by this one. Closes #4 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PqNtYNhov3fukx46KS9R7L
This commit is contained in:
@@ -433,6 +433,38 @@ async fn a_cold_ranged_request_streams_while_the_file_is_still_being_fetched() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_cold_resume_to_the_end_of_the_file_is_not_deferred() {
|
||||
// The counterpart to the bounded-range case: a range that runs to the end
|
||||
// of the file *can* be completed from the transfer, because its holdback
|
||||
// releases at the natural end. This is the shape a resumed `hf download`
|
||||
// sends, so it must keep streaming rather than be turned away with a 503.
|
||||
let hub = FakeHub::start().await;
|
||||
let weights = publish_model(&hub).await;
|
||||
hub.throttle(8 * 1024, Duration::from_millis(2)).await;
|
||||
|
||||
let bucket = bucket_dir("cold-resume-eof");
|
||||
let rf = instance(&bucket, Some(&hub), |_| {});
|
||||
|
||||
let (status, headers, body) = call(
|
||||
&rf.app,
|
||||
"GET",
|
||||
"/Qwen/Qwen3-32B/resolve/main/model.safetensors",
|
||||
&[("range", "bytes=1000-")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
status,
|
||||
StatusCode::PARTIAL_CONTENT,
|
||||
"a resume to EOF is served from the tee, not deferred"
|
||||
);
|
||||
assert_eq!(body, weights[1000..]);
|
||||
assert_eq!(
|
||||
headers["content-range"],
|
||||
format!("bytes 1000-{}/{}", weights.len() - 1, weights.len())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_ranged_request_resumes_from_the_offset_it_asks_for() {
|
||||
let hub = FakeHub::start().await;
|
||||
@@ -440,8 +472,40 @@ async fn a_ranged_request_resumes_from_the_offset_it_asks_for() {
|
||||
let bucket = bucket_dir("range");
|
||||
let rf = instance(&bucket, Some(&hub), |_| {});
|
||||
|
||||
// Cold, with a range: the whole file must be stored before any part of it
|
||||
// is served, or the bucket would hold a fragment.
|
||||
// Cold, with a range that stops short of the end. The whole file must be
|
||||
// stored before any part of it is served, or the bucket would hold a
|
||||
// fragment -- and this range cannot be *completed* from the transfer
|
||||
// either, because its last chunk is withheld until the digest is verified
|
||||
// over every byte. Rather than hand the client its slice and then stall it
|
||||
// on that tail, say so; the fetch runs on regardless.
|
||||
let (status, headers, _) = call(
|
||||
&rf.app,
|
||||
"GET",
|
||||
"/Qwen/Qwen3-32B/resolve/main/model.safetensors",
|
||||
&[("range", "bytes=100000-100999")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
|
||||
assert_eq!(headers["retry-after"], "5");
|
||||
|
||||
let oid = {
|
||||
use sha2::{Digest, Sha256};
|
||||
hex::encode(Sha256::digest(&weights))
|
||||
};
|
||||
let blob = bucket.join(key::blob(&oid));
|
||||
for _ in 0..400 {
|
||||
if blob.exists() && std::fs::metadata(&blob).unwrap().len() == weights.len() as u64 {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
assert_eq!(
|
||||
std::fs::metadata(&blob).unwrap().len(),
|
||||
weights.len() as u64,
|
||||
"a ranged cold request stores the whole file, never a fragment"
|
||||
);
|
||||
|
||||
// And the retry the 503 asked for is served from the bucket, in full.
|
||||
let (status, headers, body) = call(
|
||||
&rf.app,
|
||||
"GET",
|
||||
@@ -457,18 +521,6 @@ async fn a_ranged_request_resumes_from_the_offset_it_asks_for() {
|
||||
);
|
||||
assert_eq!(headers["content-length"], "1000");
|
||||
|
||||
let oid = {
|
||||
use sha2::{Digest, Sha256};
|
||||
hex::encode(Sha256::digest(&weights))
|
||||
};
|
||||
assert_eq!(
|
||||
std::fs::metadata(bucket.join(key::blob(&oid)))
|
||||
.unwrap()
|
||||
.len(),
|
||||
weights.len() as u64,
|
||||
"a ranged cold request stores the whole file, never a fragment"
|
||||
);
|
||||
|
||||
// Warm, resuming to the end.
|
||||
let (status, _, tail) = call(
|
||||
&rf.app,
|
||||
|
||||
Reference in New Issue
Block a user