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
gates / gates (push) Successful in 1m40s
This commit is contained in:
@@ -342,10 +342,13 @@ func (m *Manager) tier2ReconcileRoots(destBase string, roots, legRels []string)
|
||||
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) {
|
||||
// The check and the copy's record are one step under m.mu (commit security review 2026-10-23 night): a restore that
|
||||
// begins after this point sees the copy (Tier2CopyBlocksRestore), and every leg below asks again before it copies.
|
||||
if !m.beginTier2Run(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
|
||||
}
|
||||
defer tier2CopyEnd(stackName)
|
||||
// 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 {
|
||||
@@ -443,6 +446,9 @@ func (m *Manager) RunTier2(stackName string) error {
|
||||
m.logger.Printf("[WARN] [backup] Tier 2 %s: unit leg SKIPPED — the recovery unit on the source drive lists no database dumps and no volume tars, while the existing copy at %s does. The copy was PRESERVED rather than replaced with an empty one (R-403). The other legs continue.",
|
||||
stackName, destUnit)
|
||||
warns = append(warns, m.note(tier2UnitPreservedKey))
|
||||
} else if m.restoreRunningFor(stackName) {
|
||||
m.logger.Printf("[WARN] [backup] Tier 2 %s stopped before the unit leg — a restore of it began (R-1180)", stackName)
|
||||
return nil
|
||||
} else if err := mirror(unitDir, destUnit); err != nil {
|
||||
m.recordTier2Failure(stackName, target, err)
|
||||
if m.tier2Notify != nil {
|
||||
@@ -460,6 +466,10 @@ func (m *Manager) RunTier2(stackName string) error {
|
||||
warns = append(warns, m.note("note.tier2.emptied_kept", lg.DestRel))
|
||||
continue
|
||||
}
|
||||
if m.restoreRunningFor(stackName) {
|
||||
m.logger.Printf("[WARN] [backup] Tier 2 %s stopped before leg %s — a restore of it began (R-1180)", stackName, lg.DestRel)
|
||||
return nil
|
||||
}
|
||||
if err := mirror(lg.Src, dst); err != nil {
|
||||
m.recordTier2Failure(stackName, target, err)
|
||||
if m.tier2Notify != nil {
|
||||
@@ -930,11 +940,27 @@ func dirSizeBytes(dir string) int64 {
|
||||
return size
|
||||
}
|
||||
|
||||
// beginTier2Run records a second-drive run of stack as in flight — unless a restore of it runs — in ONE step under m.mu,
|
||||
// the lock the restore record lives under. The caller ends it with tier2CopyEnd(stack).
|
||||
func (m *Manager) beginTier2Run(stack string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.restoreRunningForLocked(stack) {
|
||||
return false
|
||||
}
|
||||
tier2CopyBegin(stack)
|
||||
return true
|
||||
}
|
||||
|
||||
// restoreRunningFor reports whether a restore of stack is in flight (the restore record the pages follow, R-550).
|
||||
// The shares restores are recorded under their display name.
|
||||
func (m *Manager) restoreRunningFor(stack string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.restoreRunningForLocked(stack)
|
||||
}
|
||||
|
||||
func (m *Manager) restoreRunningForLocked(stack string) bool {
|
||||
if !m.opRunning {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user