D48: a restore downloads first — every restore path resolves and pulls every image of the definition that will run before the stop, under the image-work lock held until the restore returns; a failed pull refuses with the app untouched (a deleted version and a busy registry read differently), one operator line. Red-proved. Commit only.
gates / gates (push) Successful in 1m26s

This commit is contained in:
2026-10-25 09:38:28 +01:00
parent b680db85cb
commit 2d71270858
16 changed files with 404 additions and 3 deletions
@@ -52,6 +52,16 @@ func BeginImageWork(args []string) (end func()) {
return imageWorkMu.RUnlock
}
// HoldImageWork holds the image-work lock SHARED until the returned func runs, whatever the verb (D48, 2026-10-25): a
// restore resolves and pulls its images BEFORE it stops the app and starts them minutes later, so the clean-up must
// not run anywhere in between. A compose command inside the hold takes the lock shared again (BeginImageWork); that
// nests safely because the ONLY exclusive taker is TryImageCleanup's TryLock, which never waits — so no writer can be
// queued between the two shared holds. Pinned by TestD48_HoldImageWorkBlocksCleanupAndNests.
func HoldImageWork() (end func()) {
imageWorkMu.RLock()
return imageWorkMu.RUnlock
}
// TryImageCleanup takes the image-work lock exclusively if no image work runs now. ok=false: something is
// pulling — do not clean up now.
func TryImageCleanup() (end func(), ok bool) {