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
+12
View File
@@ -340,6 +340,12 @@ func (m *Manager) tier2ReconcileRoots(destBase string, roots, legRels []string)
// capture legs (§8). Best-effort and idempotent. Records status for the UI; returns an error only on
// an actual copy failure (no valid target is a recorded status, not an error).
func (m *Manager) RunTier2(stackName string) error {
// R-1180 for EVERY caller (review 2026-10-23 night, N5): the household's „Tier 2 on" save and the debug run call
// this directly, outside RunAllTier2's skip — a mirror (rsync --delete) must never start during a restore of the app.
if m.restoreRunningFor(stackName) {
m.logger.Printf("[WARN] [backup] Tier 2 for %s not started — a restore of it is running; it is mirrored at the next run (R-1180)", stackName)
return nil
}
// Customer turned Tier 2 off for this app (config panel) — skip without touching status.
if m.settings != nil {
if cd := m.settings.GetCrossDriveConfig(stackName); cd != nil && cd.UserDisabled {
@@ -797,6 +803,10 @@ func rsyncMirror(src, dst string) error {
s, d := strings.TrimRight(src, "/"), strings.TrimRight(dst, "/")
rs := []string{"-a", "--delete", s + "/", d + "/"}
name, app := tier2CopyName(d)
// One copy per destination at a time in this process (review N4): a second run waits, it never removes the first.
lk := tier2CopyLock(name)
lk.Lock()
defer lk.Unlock()
tier2CopyBegin(app)
defer tier2CopyEnd(app)
if tier2CappedPath(s) && tier2CappedPath(d) {
@@ -813,7 +823,9 @@ func rsyncMirror(src, dst string) error {
// created empty — an empty source would let `--delete` empty the copy (review 2026-10-22 night, C4).
"--mount", "type=bind,src=" + s + ",dst=" + s + ",readonly", "--mount", "type=bind,src=" + d + ",dst=" + d,
"--entrypoint", "rsync", img}, rs...)
tier2MarkRunning(name, true) // the start-up retry never removes a copy this process runs (review N3)
out, code, err := tier2RunCapped(ctx, args)
tier2MarkRunning(name, false)
if err == nil {
return nil
}