Files
felhom-controller/controller/internal/dockerexec/imagework.go
T

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
}