fix(download): let an aborted download group actually stop its transfers - #949
Conversation
XetFileDownloadGroup::abort() unblocked the caller but the transfer kept going. A 1 GB file aborted after five seconds carried on for another three minutes at full bandwidth before the data was thrown away. Two things combined to cause it. FileDownloadSession::setup_reconstructor never called FileReconstructor::with_cancellation_token, so every reconstruction ran against a token the reconstructor had made for itself and nobody could reach; the loop checks it at every term and every buffer acquisition, but it could never be cancelled. In production with_cancellation_token was only ever called from tests. Meanwhile abort() aborted the wrapper join handle. That drops the wrapper task, which is why the Python side returned promptly, but the work the reconstruction had already spawned is not a child of that task and carried on to completion. download_file_background now takes an optional cancellation token and passes it down to the reconstructor, so cancelling stops the term loop from scheduling further ranges. The download group creates its per-file child task runtime before starting the transfer so it has a token to hand over. A cancelled download also no longer reports SizeMismatch. Stopping short is the point, so the size check is skipped once the token is cancelled rather than reporting a user abort as a corrupt file. Ranges already in flight when the token fires still finish; this stops new ones being scheduled rather than tearing down open connections. Fixes huggingface#942
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 19b079b. Configure here.
| .download_session | ||
| .download_file_background(file_info.clone(), absolute_path.clone(), Some(token.clone())) | ||
| .await?; | ||
|
|
There was a problem hiding this comment.
Errors reported as user cancellation
High Severity
The per-file TaskRuntime token is handed straight to FileReconstructor, whose RunState::set_error cancels that same token on a fetch or write failure. The mapped handle is already waiting on token.cancelled() and treats it as UserCancelled, so a real download failure is reported as a user abort and the underlying error is lost.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 19b079b. Configure here.


Fixes #942.
What was happening
XetFileDownloadGroup::abort()unblocked the caller straight away, but the transfer it wasmeant to stop carried on. The report has a 1 GB file aborted after five seconds still pulling
data for another three minutes, roughly 1.2 GB moved and then discarded.
Why
Two separate things, and each one on its own would have been enough.
FileDownloadSession::setup_reconstructornever calledFileReconstructor::with_cancellation_token. Every reconstruction therefore ran with thetoken
FileReconstructor::newbuilds for itself, which no caller holds a handle to. Thereconstruction loop does check that token, at the outer term loop, at each term, and while
waiting on the download buffer semaphore, but nothing could ever cancel it. Searching for
with_cancellation_tokenin the tree turns up three call sites, all insidemod cancellation_tests. The mechanism was live in tests and dead in production.Separately,
abort()callsdownload_join_handle.abort(). That drops the wrapper task, whichis why the Python caller returns immediately and
wait_to_finish()raises. But the work thereconstruction had already spawned is not a child of that task, so aborting the handle does
nothing to it. Tokio does not cancel the tasks a cancelled task spawned.
So the caller saw a cancelled download and the network saw a download running to completion.
The change
download_file_backgroundtakes anOption<CancellationToken>and passes it down throughdownload_file_with_idtosetup_reconstructor, which hands it to the reconstructor. Theexisting checks in the term loop then do what they were written to do.
XetFileDownloadGroup::start_download_file_to_pathcreates its per-file child task runtimebefore starting the transfer rather than after, so it has a token to pass. That reorder is the
whole change on the
xet_pkgside.One behaviour change worth calling out.
download_file_with_idvalidates the byte countagainst
file_info.file_size()and returnsDataError::SizeMismatchon a mismatch. Acancelled download stops short by design, so it tripped that check and a user abort surfaced
as a corrupt download. The size check is now skipped when the token is cancelled.
download_file_backgroundis public, soapi_changes/update_260824_download_cancellation_token.mdis included per the convention in
api_changes/README.md. The one other caller,legacy/data_client.rs, passesNoneand is unchanged in behaviour.What this does and does not do
It stops new ranges being scheduled. Ranges already in flight when the token fires still run to
completion, so abort frees bandwidth progressively rather than instantly. That matches the
issue's stated expectation ("cancel outstanding chunk transfers or prevent scheduling new
ranges") on the second count.
If you want in-flight range requests torn down as well, that is a larger change into the
transfer scheduler and I would rather do it as a follow-up than bundle it here. Happy to take
it on if you point me at where you would want the seam.
Testing
Three tests in
xet_data/src/processing/file_download_session.rs, using the existing local-CASharness, so no network:
Nonestill downloads normally, so the token is genuinely optional280 passing in
xet-dataand 142 inhf-xet.I checked the tests against the unfixed code by leaving the plumbing in place and removing only
the
with_cancellation_tokenhand-off. The cancellation test fails, the other two pass. That isthe isolation I wanted: the two that pass either way are regression cover, and the one that
flips is the one demonstrating the bug.
cargo fmt --checkis clean.cargo clippy --all-targetsreports the same two warnings infile_download_session.rsbefore and after this change, both well away from the lines touched,so I left them alone.
Note
Medium Risk
Touches core download cancellation and error classification for aborted transfers; behavior change is intentional but affects bandwidth and how partial downloads surface to callers.
Overview
Fixes aborted download groups continuing to pull data (#942) by threading a caller-owned
CancellationTokenthrough background file downloads intoFileReconstructor, so the reconstruction term loop can stop scheduling new ranges when the group is cancelled.FileDownloadSession::download_file_backgroundnow takesOption<CancellationToken>;setup_reconstructorapplieswith_cancellation_tokenwhen a token is supplied (streaming paths passNone).XetFileDownloadGroupcreates the per-file childTaskRuntimebefore starting the transfer and passestask_runtime.cancellation_token()into the session. The legacydownload_asyncpath passesNone, preserving prior behavior.When a download is stopped via cancellation,
download_file_with_idreturns bytes written so far and skips the expected-sizeSizeMismatchcheck so user abort is not reported as corruption. Anapi_changesnote documents the public signature change.Reviewed by Cursor Bugbot for commit 19b079b. Bugbot is set up for automated code reviews on this repo. Configure here.