7861bf9dde
gates / gates (push) Successful in 28s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
63 lines
2.4 KiB
Go
63 lines
2.4 KiB
Go
package dockerexec
|
|
|
|
import "sync"
|
|
|
|
// ── R-863 (v0.294.0): no image clean-up while an image is being pulled ───────────────────────────────
|
|
//
|
|
// MEASURED 2026-10-04 night drill, a fresh box: the controller's one-time image clean-up ran while the
|
|
// household's first BookStack install was inside `docker compose up -d`. compose had pulled
|
|
// `mariadb@sha256:…` (stored untagged) and not yet created the container, so no container, installed app
|
|
// or undo named it; the clean-up deleted it one second before compose created the container, and the
|
|
// install failed. An app being installed is not "installed" yet, and a restore or undo pulls the same way.
|
|
//
|
|
// THE RULE: every compose command that can pull an image and then create a container from it (up, pull,
|
|
// create, run) holds the image-work lock SHARED for its whole run; an image clean-up pass takes it
|
|
// EXCLUSIVELY, without waiting (TryLock). So a pass never runs while any such command runs, and a command
|
|
// that starts during a pass waits the few seconds the pass takes. A pass that cannot take the lock does
|
|
// not run; its caller tries again later. Pinned by TestR863_* (internal/stacks/image_retention_r863_test.go).
|
|
|
|
var imageWorkMu sync.RWMutex
|
|
|
|
// ImagePulling reports whether compose args are an image-pulling verb (up, pull, create, run). Flags
|
|
// before the verb (`-p name`, `--profile x`) are skipped.
|
|
func ImagePulling(args []string) bool {
|
|
for i := 0; i < len(args); i++ {
|
|
a := args[i]
|
|
if a == "compose" {
|
|
continue
|
|
}
|
|
if len(a) > 0 && a[0] == '-' {
|
|
switch a {
|
|
case "-p", "--project-name", "-f", "--file", "--profile", "--env-file", "--project-directory":
|
|
i++ // the flag's value
|
|
}
|
|
continue
|
|
}
|
|
switch a {
|
|
case "up", "pull", "create", "run":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// BeginImageWork holds the image-work lock shared while compose args pull images; the returned func
|
|
// releases it. For any other verb it returns a no-op.
|
|
func BeginImageWork(args []string) (end func()) {
|
|
if !ImagePulling(args) {
|
|
return 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) {
|
|
if !imageWorkMu.TryLock() {
|
|
return nil, false
|
|
}
|
|
return imageWorkMu.Unlock, true
|
|
}
|