Second-drive copy: two races closed — the start-up/retry removal holds the destination's lock (a copy this process starts is never removed), and a run records itself in one step with the restore check, every leg asking again; red-proved, race-clean; unreleased
gates / gates (push) Successful in 1m40s

This commit is contained in:
2026-10-22 22:58:33 +02:00
parent 9d9e4041c9
commit 1babfe94f6
5 changed files with 111 additions and 2 deletions
@@ -232,6 +232,11 @@ func tier2RetryOnce(ctx context.Context) bool {
// tier2RemoveCopyQuiet is the retry's removal: a failure keeps the stray without starting another retry.
func tier2RemoveCopyQuiet(ctx context.Context, name string) {
lk := tier2CopyLock(name)
if !lk.TryLock() {
return // a copy of that destination runs now: it removes the stray itself before it starts
}
defer lk.Unlock()
rctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
out, err := tier2Docker(rctx, "rm", "-f", name)
@@ -278,15 +283,23 @@ func clearLeftoverTier2CopiesOnce(ctx context.Context) error {
}
}
tier2Copies.Unlock()
// The check above and the removal below are not atomic: a copy of the same destination may start in between. Each
// removal holds that destination's lock (TryLock: a copy of it runs in this process → skipped), so it can never hit
// a copy this process starts (commit security review 2026-10-23 night).
if len(names) == 0 {
tier2Logf("[INFO] [tier2] start-up: no leftover second-drive copy container")
return nil
}
failed := 0
for _, n := range names {
lk := tier2CopyLock(n)
if !lk.TryLock() {
continue // this process copies into that destination right now — its own container, not a leftover
}
rctx, rcancel := context.WithTimeout(ctx, 60*time.Second)
rout, rerr := tier2Docker(rctx, "rm", "-f", n)
rcancel()
lk.Unlock()
if rerr != nil {
failed++
tier2Logf("[ERROR] [tier2] start-up: could not remove the leftover copy container %s: %v %s", n, rerr, strings.TrimSpace(string(rout)))