Skip to content

fix(model-install): keep restored downloads restartable - #9521

Open
primorLee wants to merge 2 commits into
invoke-ai:mainfrom
primorLee:codex/fix-restored-install-restart
Open

primorLee wants to merge 2 commits into
invoke-ai:mainfrom
primorLee:codex/fix-restored-install-restart

Conversation

@primorLee

Copy link
Copy Markdown

Summary

Fix restored installs that receive a non-resumable response before their first download-started signal.

  • Share a newly-created multifile download's parts with the public install job before queue submission
  • Keep pre-start resume_required state visible to the API and available to the install marker and restart path
  • Add a regression covering enqueue, pre-start cancellation, public serialization, marker persistence, and restart

Related Issues / Discussions

Closes #9480

QA Instructions

uv run --frozen --extra test pytest tests/app/services/model_install/test_model_install.py::test_restart_failed_uses_parts_created_before_download_started -q
INVOKEAI_ALLOW_PRIVATE_DOWNLOAD_URLS=true uv run --frozen --extra test pytest tests/app/services/model_install/test_model_install.py -q
INVOKEAI_ALLOW_PRIVATE_DOWNLOAD_URLS=true ./.venv/bin/pytest tests/app/services/download/test_download_queue.py -q -k 'not download_refuses_non_public_source and not download_refuses_redirect_to_non_public_address and not rejected_redirect_closes_streamed_response and not download_refuses_multi_hop_redirect_to_non_public_address'
./.venv/bin/pytest tests/app/services/download/test_download_queue.py::test_download_refuses_non_public_source tests/app/services/download/test_download_queue.py::test_download_refuses_redirect_to_non_public_address tests/app/services/download/test_download_queue.py::test_rejected_redirect_closes_streamed_response tests/app/services/download/test_download_queue.py::test_download_refuses_multi_hop_redirect_to_non_public_address -q
./.venv/bin/ruff check invokeai/app/services/model_install/model_install_default.py tests/app/services/model_install/test_model_install.py
./.venv/bin/ruff format --check invokeai/app/services/model_install/model_install_default.py tests/app/services/model_install/test_model_install.py

The focused regression passes, the full model-install test file passes 36 tests, and the related download-queue file passes all 36 tests when split between its mocked private-address cases and its four default-policy SSRF refusal cases. The split is needed locally because the sandbox maps public test hostnames into the reserved 198.18.0.0/15 range.

Merge Plan

No special merge sequencing is required.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • Redux migration is not applicable; no redux slice changed
  • Documentation is not applicable; behavior and public interfaces are unchanged
  • What's New copy is not applicable; this is not a release PR

@github-actions github-actions Bot added python PRs that change python files services PRs that change app services python-tests PRs that change python tests labels Aug 20, 2026
@lstein lstein moved this to 6.14.1: Bug fixes to 6.14.0 in Invoke - Community Roadmap Aug 24, 2026
@lstein lstein added the 6.14.2 label Sep 21, 2026
@lstein lstein assigned lstein and unassigned dunkeroni Sep 21, 2026

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for picking up #9480. The fix works for the path the issue describes, but it breaks one existing behavior, so I'm requesting one change before merge. Reviewed at 19ef9e9.

What works

  • The #9480 path is fixed. A restored install is resumed and its first file gets a 416 before any download-started signal. It now goes PAUSED with its parts visible, restart_failed re-queues them, and the PAUSED marker keeps each file's etag/canonical_url/download_path instead of saving files: [].
  • The regression test is meaningful. It fails with the new line removed and passes with it. The full test_model_install.py passes at the PR head.
  • No new thread-safety risk. _download_started_callback already makes this exact assignment, so the install job and the multifile job already shared the same parts set. Nothing adds or removes parts after multifile_download() builds it.

Blocker: resuming no longer warns when a download restarted from scratch

_resume_remote_download (model_install_default.py:314-323) checks the job's current parts. For any part whose .downloading file has disappeared, it sets resume_from_scratch = True and resume_message = "Partial file missing. ...". It then calls _enqueue_remote_download. With this PR, that call replaces job.download_parts with brand-new parts before the resume route returns. The route returns job, and the UI decides whether to warn with hasRestartedFromScratch(job) on that response (ModelInstallQueueItem.tsx ~L194), so it no longer sees the flag.

The download queue has its own fallback (download_default.py:455), but it only fires when the part has an etag or last_modified (had_resume_metadata). For a job paused during the current session, job._resume_metadata is None, so the fresh parts have neither and the fallback never fires. The user sees "Download resumed" instead of the "restarted from scratch" warning, and lines 314-323 now write to parts that are thrown away immediately.

How to trigger it:

  1. Start a URL/HF install and let a file download partially (the started callback fills in job.download_parts).
  2. Pause the install.
  3. Delete that file's .downloading file from the install tmpdir.
  4. Click Resume. The response carries the fresh parts with no resume_from_scratch, so the UI shows the success toast.

I confirmed this with a throwaway test. On this branch it fails; with the new line removed it passes:

def test_resume_reports_restart_from_scratch(mm2_installer, tmp_path, monkeypatch):
    source = URLModelSource(url=Url("https://example.com/model.safetensors"))
    old = DownloadJob(source=source.url, dest=tmp_path / "model.safetensors")
    old.bytes = 4
    old.download_path = tmp_path / "model.safetensors"  # no .downloading on disk
    job = ModelInstallJob(id=4242, source=source, config_in=ModelRecordChanges(), local_path=tmp_path)
    job._install_tmpdir = tmp_path
    job.download_parts = {old}
    job.status = InstallStatus.PAUSED
    rf = RemoteModelFile(url=source.url, path=Path("model.safetensors"), size=8)
    monkeypatch.setattr(mm2_installer, "_remote_files_from_source", lambda _: ([rf], None))
    monkeypatch.setattr(mm2_installer._download_queue, "submit_multifile_download", MagicMock())
    mm2_installer.resume_job(job)
    assert any(p["resume_from_scratch"] for p in job.model_dump(mode="json")["download_parts"])

Suggested fix: in _resume_remote_download, collect the from-scratch flags from the old parts by str(part.source). After _enqueue_remote_download returns, copy resume_from_scratch and resume_message onto the matching new parts in job.download_parts. Please add the test above, or a similar one.

Non-blocking

  1. A restored install that was already PAUSED still can't be restarted directly. If the app quits after the 416 pause, the marker says paused. On the next start, _restore_incomplete_installs stops at if job.paused: continue before anything is queued, so download_parts stays empty. restart_failed still does nothing, and the UI hides Restart because it only checks the parts. Resume → 416 again → Restart works around it. Filling download_parts from the marker's files metadata on restore, or letting restart_failed use _resume_metadata, would close this. #9480's second suggestion (never overwrite a marker's files with []) is also still open in general, though I found no UI path that reaches it today.
  2. The regression test skips the restore path. It calls _enqueue_remote_download directly on a fresh job and sets the etag on the part by hand. It never exercises _restore_incomplete_installs_resume_remote_download with a real marker's resume data, which is the scenario in the issue. A variant that writes a marker, restores it, and then does the 416 pause and restart would pin the actual bug.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.2 python PRs that change python files python-tests PRs that change python tests services PRs that change app services

Projects

Status: 6.14.1: Bug fixes to 6.14.0

Development

Successfully merging this pull request may close these issues.

Restored install that pauses before its first download-started signal cannot be restarted (restart_failed no-ops, marker loses resume metadata)

3 participants