controller v0.296.0: a backup run cut off by a power cut or restart is said on the backup pages (R-519); the whole-system backup text states today's measurement (R-518)
gates / gates (push) Successful in 30s
gates / gates (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -299,6 +299,9 @@ type Manager struct {
|
||||
// R-550: persistence of the above (restore_record.go) and the per-app interrupted notices.
|
||||
opRecordPath string
|
||||
opInterrupted map[string]RestoreOpResult
|
||||
// R-519: the app-data run record (run_record.go) and the start of the last run a stop cut off (zero = none).
|
||||
runRecordPath string
|
||||
runInterrupted time.Time
|
||||
|
||||
// Cached status for page rendering (refreshed periodically)
|
||||
cachedStatus *FullBackupStatus
|
||||
@@ -311,9 +314,13 @@ type FullBackupStatus struct {
|
||||
Running bool
|
||||
|
||||
// DB Dumps
|
||||
LastDBDump *DBDumpStatus
|
||||
DumpFiles []DumpFileInfo
|
||||
DiscoveredDBs []DiscoveredDB
|
||||
LastDBDump *DBDumpStatus
|
||||
// InterruptedRunAt (R-519): the start of the last app-data run a power cut or a restart cut off; zero = the
|
||||
// newest run finished. Shown on /backups and /backups/apps until a run ends with every step OK.
|
||||
InterruptedRunAt time.Time
|
||||
Interrupted bool // InterruptedRunAt is set (the template gate: a time.Time is always "true" to `if`)
|
||||
DumpFiles []DumpFileInfo
|
||||
DiscoveredDBs []DiscoveredDB
|
||||
|
||||
// Schedule
|
||||
DBDumpSchedule string
|
||||
@@ -548,6 +555,11 @@ func (m *Manager) beginOffsiteRunStamp(runID string) func() {
|
||||
func (m *Manager) runDBDumpsInternal(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
m.logger.Printf("[INFO] [backup] Starting database dump run")
|
||||
// R-519: the run is on record while it runs; a stop before markRunEnded leaves it "running", which the next
|
||||
// start turns into the page's interrupted-run notice (run_record.go). Every return path ends the record.
|
||||
m.markRunStarted(start)
|
||||
runOK := false
|
||||
defer func() { m.markRunEnded(runOK) }()
|
||||
|
||||
// R-181: open the per-run admission scope HERE, because this function is the single orchestrator
|
||||
// of all three write legs. Each app's reserve verdict is taken at its first write of this run and
|
||||
@@ -688,6 +700,7 @@ func (m *Manager) runDBDumpsInternal(ctx context.Context) error {
|
||||
// residue, invisible in the snapshot list. Guarded (deployed + different current drive only).
|
||||
m.pruneStalePrimaryDirs()
|
||||
|
||||
runOK = allOK
|
||||
// No silent partials: a DB-dump or volume-dump failure fails the whole run.
|
||||
if !allOK {
|
||||
return fmt.Errorf("some backup steps failed: %s", strings.Join(failedSummaryLines(summary), "; "))
|
||||
@@ -1210,6 +1223,7 @@ func (m *Manager) RefreshCache(nextDBDump time.Time) {
|
||||
m.mu.Lock()
|
||||
status.Running = m.running
|
||||
status.LastDBDump = m.lastDBDump
|
||||
status.InterruptedRunAt, status.Interrupted = m.runInterrupted, !m.runInterrupted.IsZero()
|
||||
|
||||
// Cross-check lastDBDump results inside lock to prevent torn writes.
|
||||
if m.lastDBDump != nil && len(files) > 0 {
|
||||
@@ -1252,7 +1266,8 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
|
||||
// Update dynamic fields that don't need subprocess calls
|
||||
status.Running = m.running
|
||||
status.NextDBDump = nextDBDump
|
||||
status.SingleCopyWarning = m.singleCopyWarning() // F6: honest single-drive signal
|
||||
status.SingleCopyWarning = m.singleCopyWarning() // F6: honest single-drive signal
|
||||
status.InterruptedRunAt, status.Interrupted = m.runInterrupted, !m.runInterrupted.IsZero() // R-519
|
||||
// Deep-copy lastDBDump so callers cannot mutate shared state.
|
||||
if m.lastDBDump != nil {
|
||||
copyDump := *m.lastDBDump
|
||||
@@ -1278,10 +1293,12 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
|
||||
latestTime = f.ModTime
|
||||
}
|
||||
}
|
||||
// R-519: after a restart the run's result is not in memory, so it is SYNTHESISED from the files. A run
|
||||
// the stop cut off left a fresh .sql beside last night's tars — "Success" must not be read off that.
|
||||
status.LastDBDump = &DBDumpStatus{
|
||||
LastRun: latestTime,
|
||||
Results: results,
|
||||
Success: true,
|
||||
Success: m.runInterrupted.IsZero(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1295,6 +1312,8 @@ func (m *Manager) GetFullStatus(nextDBDump time.Time) *FullBackupStatus {
|
||||
DBDumpSchedule: m.cfg.Backup.DBDumpSchedule,
|
||||
NextDBDump: nextDBDump,
|
||||
SingleCopyWarning: m.singleCopyWarning(), // F6
|
||||
InterruptedRunAt: m.runInterrupted, // R-519
|
||||
Interrupted: !m.runInterrupted.IsZero(),
|
||||
}
|
||||
if m.lastDBDump != nil {
|
||||
copyDump := *m.lastDBDump
|
||||
|
||||
Reference in New Issue
Block a user