You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When any single part of a multi-file install errors, _download_error_callback deletes the entire install tmpdir (model_install_default.py:1482-1489 → _safe_rmtree(install_job._install_tmpdir)) — including parts that finished completely and partials holding gigabytes of resumable progress. One transient failure (a 5xx on one file, a rename race) throws away everything the resume machinery exists to protect.
Found during the adversarial review of #9432; the rmtree behavior predates that PR, but #9432 added a new way to trip it (the sidecar rename race below).
Mechanism
Worker thread: any exception other than DownloadJobCancelledException in _do_download marks the part ERROR (download_default.py:327-330). For a part belonging to an install, _download_error_callback then pops the install job, sets it errored, cancels the multifile job, and rmtrees the whole _install_tmpdir.
Two concrete triggers:
Transient server error on one file. A single HTTP 500 on file k of an n-file install (raised at download_default.py:456-459) deletes the completed files 1..k-1 and all partial progress. The next attempt starts the entire install from zero.
Sidecar rename race (new surface from fix(download): finalize completed range resumes #9432). The 416-promotion path stats the sidecar (download_default.py:352) and renames it (download_default.py:446) a full network round-trip apart, with no existence guard. If the sidecar vanishes in between — user-triggered restart_file() runs clear_partials=True (model_install_default.py:665, :1314) against an in-flight request; manual deletion; AV/indexer interference on Windows — the rename raises FileNotFoundError → part ERROR → whole tmpdir deleted.
Reproduction (test sketch, deterministic)
Trigger 1 needs only two mounts — one good file, one 500 — through the install service, then assert the tmpdir is gone despite file 1 having completed.
Trigger 2 can be made deterministic at the download-queue level with an adapter that deletes the sidecar during the request, simulating the race:
In _download_error_callback, preserve the tmpdir when any part has resumable progress (a .downloading file on disk) or has already completed: write the install marker with an errored/paused status instead of rmtree, so the existing restore/restart_failed machinery can pick it up. Only rmtree when nothing on disk is worth keeping.
Guard the 416 promotion rename: if the sidecar is missing at rename time, fall through to the pause/restart path instead of letting FileNotFoundError escalate to a part ERROR.
Summary
When any single part of a multi-file install errors,
_download_error_callbackdeletes the entire install tmpdir (model_install_default.py:1482-1489→_safe_rmtree(install_job._install_tmpdir)) — including parts that finished completely and partials holding gigabytes of resumable progress. One transient failure (a 5xx on one file, a rename race) throws away everything the resume machinery exists to protect.Found during the adversarial review of #9432; the rmtree behavior predates that PR, but #9432 added a new way to trip it (the sidecar rename race below).
Mechanism
Worker thread: any exception other than
DownloadJobCancelledExceptionin_do_downloadmarks the part ERROR (download_default.py:327-330). For a part belonging to an install,_download_error_callbackthen pops the install job, sets it errored, cancels the multifile job, and rmtrees the whole_install_tmpdir.Two concrete triggers:
HTTP 500on file k of an n-file install (raised atdownload_default.py:456-459) deletes the completed files 1..k-1 and all partial progress. The next attempt starts the entire install from zero.download_default.py:352) and renames it (download_default.py:446) a full network round-trip apart, with no existence guard. If the sidecar vanishes in between — user-triggeredrestart_file()runsclear_partials=True(model_install_default.py:665,:1314) against an in-flight request; manual deletion; AV/indexer interference on Windows — the rename raisesFileNotFoundError→ part ERROR → whole tmpdir deleted.Reproduction (test sketch, deterministic)
Trigger 1 needs only two mounts — one good file, one 500 — through the install service, then assert the tmpdir is gone despite file 1 having completed.
Trigger 2 can be made deterministic at the download-queue level with an adapter that deletes the sidecar during the request, simulating the race:
Suggested fix
_download_error_callback, preserve the tmpdir when any part has resumable progress (a.downloadingfile on disk) or has already completed: write the install marker with an errored/paused status instead of rmtree, so the existing restore/restart_failedmachinery can pick it up. Only rmtree when nothing on disk is worth keeping.FileNotFoundErrorescalate to a part ERROR.