Second review of the night's own code: a stray clears when a later removal works (N2), the start-up check never removes this process's own copy (N3), one copy per destination at a time (N4), RunTier2 refuses during a restore for every caller (N5), the import refusal no longer advises removal (N9); red-proved, race-clean; README; REPORT; unreleased
gates / gates (push) Successful in 1m40s

This commit is contained in:
2026-10-22 22:45:42 +02:00
parent 943c517550
commit 9d9e4041c9
9 changed files with 341 additions and 62 deletions
@@ -33,7 +33,9 @@ func strayCopySeams(t *testing.T, root string, run func(ctx context.Context, arg
fn func(args []string) ([]byte, error)) (*dockerRec, *dockerRec) {
t.Helper()
runs, lifecycle := &dockerRec{}, &dockerRec{}
oldRoot, oldImg, oldRun, oldLog, oldDocker, oldTO := tier2CappedRoot, tier2SelfImage, tier2RunCapped, tier2Logf, tier2Docker, tier2CopyTimeout
oldRoot, oldImg, oldRun, oldLog, oldDocker, oldTO, oldRetry := tier2CappedRoot, tier2SelfImage, tier2RunCapped, tier2Logf, tier2Docker, tier2CopyTimeout, tier2RetryEvery
tier2RetryEvery = time.Hour
tier2RetryOff = true // the background retry never starts in a test (it would reach the real Docker later)
tier2CappedRoot = root
tier2SelfImage = func() string { return "sha256:abc" }
tier2RunCapped = func(ctx context.Context, args []string) ([]byte, int, error) {
@@ -49,10 +51,11 @@ func strayCopySeams(t *testing.T, root string, run func(ctx context.Context, arg
return fn(args)
}
tier2Logf = func(string, ...any) {}
tier2SetStray(false)
tier2ResetForTest()
t.Cleanup(func() {
tier2CappedRoot, tier2SelfImage, tier2RunCapped, tier2Logf, tier2Docker, tier2CopyTimeout = oldRoot, oldImg, oldRun, oldLog, oldDocker, oldTO
tier2SetStray(false)
tier2CappedRoot, tier2SelfImage, tier2RunCapped, tier2Logf, tier2Docker, tier2CopyTimeout, tier2RetryEvery = oldRoot, oldImg, oldRun, oldLog, oldDocker, oldTO, oldRetry
tier2RetryOff = false
tier2ResetForTest()
})
return runs, lifecycle
}
@@ -210,3 +213,95 @@ func TestStrayCopy_ARestoreIsRefusedWhileACopyRuns(t *testing.T) {
t.Fatal("after the copy a restore must be allowed")
}
}
// Review 2026-10-23 night. N2: a stray clears when a later removal of that name works (it used to block every restore
// until a restart). N3: the start-up check never removes a copy THIS process runs. N4: a second copy into the same
// destination waits for the first instead of removing its container. RED-PROOF: with the stray delete on a successful
// retry removed → StrayClears fails; with the `running` skip removed → OwnCopyKept fails; with the per-destination lock
// removed → SameDestinationWaits fails — each seen failing 2026-10-23 night.
func TestStrayCopy_AStrayClearsWhenALaterRemovalWorks(t *testing.T) {
root, src, dst := appDirs(t)
fail := true
strayCopySeams(t, root, func(context.Context, []string) ([]byte, int, error) { return nil, 0, nil },
func(args []string) ([]byte, error) {
if fail {
return []byte("Error response from daemon: cannot remove"), errors.New("exit 1")
}
return nil, nil
})
_ = rsyncMirror(src, dst)
if _, blocked := (&Manager{}).Tier2CopyBlocksRestore(); !blocked {
t.Fatal("fixture: an unremovable leftover must refuse restores")
}
fail = false
if !tier2RetryOnce(context.Background()) {
t.Fatal("the retry must report nothing left once the removal works")
}
if _, blocked := (&Manager{}).Tier2CopyBlocksRestore(); blocked {
t.Fatal("after the stray is removed, restores must be allowed again — without a controller restart")
}
}
func TestStrayCopy_TheStartUpCheckKeepsThisProcessesOwnCopy(t *testing.T) {
_, life := strayCopySeams(t, t.TempDir()+"/", nil, func(args []string) ([]byte, error) {
if args[0] == "ps" {
return []byte("felhom-tier2-copy-mine-00000001\nfelhom-tier2-copy-old-00000002\n"), nil
}
return nil, nil
})
tier2MarkRunning("felhom-tier2-copy-mine-00000001", true)
if err := clearLeftoverTier2CopiesOnce(context.Background()); err != nil {
t.Fatal(err)
}
got := life.all()
if strings.Contains(got, "rm -f felhom-tier2-copy-mine-00000001") || !strings.Contains(got, "rm -f felhom-tier2-copy-old-00000002") {
t.Fatalf("the check must remove only the leftover, never this process's running copy:\n%s", got)
}
}
func TestStrayCopy_ASecondCopyOfTheSameDestinationWaits(t *testing.T) {
root, src, dst := appDirs(t)
var mu sync.Mutex
order := []string{}
release := make(chan struct{})
first := true
runs, _ := strayCopySeams(t, root, func(context.Context, []string) ([]byte, int, error) {
mu.Lock()
me := first
first = false
order = append(order, "start")
mu.Unlock()
if me {
<-release
}
mu.Lock()
order = append(order, "end")
mu.Unlock()
return nil, 0, nil
}, func(args []string) ([]byte, error) {
mu.Lock()
order = append(order, strings.Join(args, " "))
mu.Unlock()
return nil, nil
})
d1, d2 := make(chan error), make(chan error)
go func() { d1 <- rsyncMirror(src, dst) }()
for len(strings.Split(runs.all(), "\n")) < 1 || runs.all() == "" {
time.Sleep(5 * time.Millisecond)
}
go func() { d2 <- rsyncMirror(src, dst) }()
time.Sleep(100 * time.Millisecond)
mu.Lock()
during := strings.Join(order, "|")
mu.Unlock()
if strings.Count(during, "rm -f") != 1 {
t.Fatalf("while the first copy runs, the second must not touch its container: %s", during)
}
close(release)
if err := <-d1; err != nil {
t.Fatal(err)
}
if err := <-d2; err != nil {
t.Fatal(err)
}
}