Skip to content

fix(upgrade): reclaim what a failed pull leaves behind - #3415

Open
vpetersson-bot wants to merge 2 commits into
masterfrom
agent/anthias-claude/e3d3f7c3f180
Open

vpetersson-bot wants to merge 2 commits into
masterfrom
agent/anthias-claude/e3d3f7c3f180

Conversation

@vpetersson-bot

Copy link
Copy Markdown
Contributor

Issues Fixed

Fixes #3410.

Description

A docker compose pull that dies partway abandons the bytes it already fetched in containerd's content store. Nothing references them, docker images cannot see them, and they do not expire. The one command that would collect them is purge_stale_images.sh, which is deliberately the last step of an upgrade and only runs once the new stack is up — so it never runs on the path that needs it. A device upgrading because it is short of space has less room on each retry than on the one before: a ratchet, not a plateau.

  • Reclaim on the pull's failure path. A new --untagged-only restricts the sweep to unreferenced untagged content, so the superseded images — the release the device is still running — survive. That is the property the deferred ordering exists to protect, and keeping it is what makes reclaiming this early safe.
  • Report what was reclaimed. docker image prune frees this data without counting it and prints Total reclaimed space: 0B, so the script measures the content store itself. On a device that is out of space, that number is the whole point.
  • Restart the services the low-RAM branch stops before pulling. A failed pull previously left the viewer stopped until reboot; the signature-check failure path already did this, so the two now share one helper.

Not included: the pre-flight free-space check the issue floats as a companion. It is a behaviour change that can wrongly block an upgrade if the estimate is off, and is better judged on its own.

Verification

Reproduced and verified end to end on the Pi 5 and x86 testbeds, both on the containerd image store (Docker 29.6.1).

Interrupting a pull on the Pi 5, before the fix:

                        content store      df available
baseline                   774,796 kB        6,458,796 kB
after interrupted pull     947,984 kB        6,285,596 kB   (+173 MB, no image to show for it)
+40s, no command run       947,976 kB        6,285,612 kB   (does not expire)
docker image prune -f  ->  "Total reclaimed space: 0B"
after prune                774,796 kB        6,458,792 kB   (169 MB actually freed, reported as 0B)

The leftovers split two ways: 130 MB of part-written layers under ingest/, and 39 MB of whole layers under blobs/ that finished downloading before the pull gave up. Measuring only ingest/ would have under-reported by 23%, which is why this counts the whole content store.

After the fix, running the upgrade against a tag that cannot be pulled, with the leftovers of an earlier failed attempt already on disk:

The pull failed; reclaiming what it left behind.
Reclaimed 168 MB left behind by an interrupted download.
--- upgrade exit status: 1 ---

Content store back to its 774,796 kB baseline; all four containers still Up 40 hours; /api/v2/assets → 200; all six Anthias images intact. Same result on x86 (prune reported 0B while freeing 37 MB; the failure path reclaimed and reported it, exit 1).

Also checked that --untagged-only spares a superseded, unreferenced Anthias image while the unflagged run still removes it, and that an unknown flag exits 2.

Checklist

  • I have performed a self-review of my own code.
  • New and existing unit tests pass locally and on CI with my changes.
  • I have done an end-to-end test for Raspberry Pi devices.
  • I have tested my changes for x86 devices.
  • I added a documentation for the changes I have made (when necessary).

🤖 Generated with Claude Code

A `docker compose pull` that dies partway abandons the bytes it already
fetched in containerd's content store, where no image references them
and `docker images` cannot see them. The cleanup that would collect them
is deliberately the last step of the upgrade and only runs once the new
stack is up, so it never runs on the path that needs it. A device
upgrading because it is short of space therefore has less room on each
retry than on the one before.

- Reclaim on the pull's failure path, restricted to unreferenced
  untagged content via a new `--untagged-only` so the release the device
  is still running survives, which is why the sweep is otherwise
  deferred.
- Report what was reclaimed: `docker image prune` frees this data
  without counting it and prints `0B`, so measure the content store
  directly.
- Restart the services the low-RAM branch stopped before the pull, as
  the signature-check failure path already did.

Measured on a Pi 5: an interrupted pull left 130 MB part-written and
39 MB of orphaned whole layers; prune freed all 169 MB and reported 0B.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
@vpetersson-bot
vpetersson-bot requested a review from a team as a code owner October 24, 2026 07:23
Copilot AI lite review requested due to automatic review settings October 24, 2026 07:23
@vpetersson-bot vpetersson-bot self-assigned this Oct 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

@vpetersson-bot
vpetersson-bot requested a lite review from Copilot October 24, 2026 07:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

All nine were on lines this branch adds, so they are fair.

- Use `[[` for the conditionals introduced here (shelldre:S7688).
  Both scripts already mix `[` and `[[`; the pre-existing `[` tests
  are left alone rather than churned into this diff.
- Give the two new functions an explicit `return` rather than
  inheriting the last command's status (shelldre:S7682).

No behaviour change: the suite drives the real script, and all 15
tests still pass. shellcheck output is byte-identical to master's, so
nothing here adds a finding of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
Copilot AI lite review requested due to automatic review settings October 24, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot encountered an error: Your billing is not configured or you have Copilot licenses from multiple standalone organizations or enterprises. To use premium requests, select a billing entity via the GitHub site, under Settings > Copilot > Features.

@sonarqubecloud

Copy link
Copy Markdown

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A failed upgrade leaves partial image data behind, so the next attempt has less room than the last

2 participants