v0.168.0: customer-configurable backup window (Mentési időablak)
ONE setting (window start W) drives every nightly leg at fixed, never-stored offsets: DB dump at W, tier-2 at W+60m, off-box at W+105m (wrap-safe). Precedence settings > controller.yaml db_dump_schedule > 02:30. - scheduler.UpdateDaily: retime a daily job at runtime (no restart) via a per-job buffered resched chan + a select case in runDailyJob. - new pure package internal/backupwindow (LegTimes/GateWindow/EffectiveWindow). - quiesce disk-tier window gate: scheduled cycles run only inside [W+2h,W+6h) with a safety valve (age>cadence+24h runs regardless); manual TriggerNow never gated. Backend.Due now also returns the backup age (from the agent's own /backup/due). - backup page: Mentési időablak card (time input + derived leg/gate rows); POST /backups/window validates -> saves -> UpdateDaily x3 -> flash. Tests: 5 groups, all red-proofed. Agent/cadence//backup/due untouched.
This commit is contained in:
@@ -25,6 +25,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appexport"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/assets"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backupwindow"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/bootrecon"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/bootstrap"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/channelhealth"
|
||||
@@ -224,7 +225,7 @@ func main() {
|
||||
// --- Quiesce loop (slice 8B): app-consistent backup around the agent vzdump ---
|
||||
// Runs only when the local API is configured (a provisioned guest) and quiesce is enabled.
|
||||
// Recover FIRST (restart any stacks left stopped by a crash mid-quiesce), then start the loop.
|
||||
quiesceLoop := startQuiesceLoop(ctx, cfg, stackMgr, logger)
|
||||
quiesceLoop := startQuiesceLoop(ctx, cfg, sett, stackMgr, logger)
|
||||
|
||||
// --- R-52: boot desired-state reconciliation ---
|
||||
// A deployed app that missed its boot start used to stay down until a human noticed (F5: immich
|
||||
@@ -575,9 +576,15 @@ func main() {
|
||||
|
||||
// Backup daily jobs
|
||||
if cfg.Backup.Enabled && backupMgr != nil {
|
||||
// v0.168.0: ONE customer setting (the window start W) drives all three nightly legs at fixed
|
||||
// offsets — db-dump at W, tier-2 at W+60m, off-box at W+105m — so they can never be misordered.
|
||||
// The window resolves settings > controller.yaml > "02:30"; a UI save fans out via UpdateDaily.
|
||||
win := backupwindow.EffectiveWindow(sett.GetBackupWindowStart(), cfg.Backup.DBDumpSchedule)
|
||||
dbLeg, tier2Leg, offboxLeg := backupwindow.LegTimes(win)
|
||||
|
||||
// App-data backup: daily database dumps. Disk-tier (restic snapshots,
|
||||
// cross-drive, integrity check, infra backup) has moved to the host agent.
|
||||
sched.Daily("db-dump", cfg.Backup.DBDumpSchedule, func(ctx context.Context) error {
|
||||
sched.Daily("db-dump", dbLeg, func(ctx context.Context) error {
|
||||
err := backupMgr.RunDBDumps(ctx)
|
||||
if err != nil {
|
||||
notifier.NotifyDBDumpFailed("Adatbázis mentés sikertelen", err.Error())
|
||||
@@ -587,10 +594,11 @@ func main() {
|
||||
return err
|
||||
})
|
||||
|
||||
// Cache refresh: every 5 minutes
|
||||
// Cache refresh: every 5 minutes. Recompute the effective window each pass so the cached
|
||||
// "next DB dump" follows a runtime window change (the UI save also refreshes immediately).
|
||||
sched.Every("backup-cache", 5*time.Minute, func(ctx context.Context) error {
|
||||
nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule)
|
||||
backupMgr.RefreshCache(nextDBDump)
|
||||
curDB, _, _ := backupwindow.LegTimes(backupwindow.EffectiveWindow(sett.GetBackupWindowStart(), cfg.Backup.DBDumpSchedule))
|
||||
backupMgr.RefreshCache(scheduler.NextDailyRun(curDB))
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -613,7 +621,7 @@ func main() {
|
||||
})
|
||||
}
|
||||
})
|
||||
sched.Daily("tier2-backup", "03:30", func(ctx context.Context) error {
|
||||
sched.Daily("tier2-backup", tier2Leg, func(ctx context.Context) error {
|
||||
backupMgr.RunAllTier2()
|
||||
return nil
|
||||
})
|
||||
@@ -646,7 +654,7 @@ func main() {
|
||||
"A távoli mentési tároló visszaállítva: a régi előzmény félretéve (nem törölve), és egy üres, új tároló jött létre a mostani kulccsal.", map[string]string{"renamed_to": renamedTo})
|
||||
}
|
||||
})
|
||||
sched.Daily("offbox-backup", "04:15", func(ctx context.Context) error {
|
||||
sched.Daily("offbox-backup", offboxLeg, func(ctx context.Context) error {
|
||||
t := sett.GetOffboxTarget()
|
||||
if t == nil || !t.Enabled || t.Schedule != "daily" || !backupMgr.OffboxConfigured() {
|
||||
return nil // not configured / not scheduled
|
||||
@@ -830,8 +838,8 @@ func main() {
|
||||
// Initial backup cache population (don't block startup)
|
||||
if cfg.Backup.Enabled && backupMgr != nil {
|
||||
go func() {
|
||||
nextDBDump := scheduler.NextDailyRun(cfg.Backup.DBDumpSchedule)
|
||||
backupMgr.RefreshCache(nextDBDump)
|
||||
curDB, _, _ := backupwindow.LegTimes(backupwindow.EffectiveWindow(sett.GetBackupWindowStart(), cfg.Backup.DBDumpSchedule))
|
||||
backupMgr.RefreshCache(scheduler.NextDailyRun(curDB))
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -1652,9 +1660,9 @@ func fileExists(path string) bool {
|
||||
// agentapi response structs).
|
||||
type quiesceBackend struct{ c *agentapi.Client }
|
||||
|
||||
func (b quiesceBackend) Due(ctx context.Context) (bool, error) {
|
||||
func (b quiesceBackend) Due(ctx context.Context) (bool, *int64, error) {
|
||||
r, err := b.c.BackupDue(ctx)
|
||||
return r.Due, err
|
||||
return r.Due, r.AgeSecs, err
|
||||
}
|
||||
func (b quiesceBackend) StartBackup(ctx context.Context) (string, error) {
|
||||
r, err := b.c.StartBackup(ctx)
|
||||
@@ -1668,7 +1676,7 @@ func (b quiesceBackend) BackupStatus(ctx context.Context) (string, error) {
|
||||
// startQuiesceLoop wires + starts the slice-8B quiesce loop when the local API is configured and
|
||||
// quiesce is enabled. It Recovers (restarts stacks left stopped by a mid-quiesce crash) before
|
||||
// starting the loop goroutine. Non-fatal: any misconfig disables the loop with a log line.
|
||||
func startQuiesceLoop(ctx context.Context, cfg *config.Config, stackMgr *stacks.Manager, logger *log.Logger) *quiesce.Loop {
|
||||
func startQuiesceLoop(ctx context.Context, cfg *config.Config, sett *settings.Settings, stackMgr *stacks.Manager, logger *log.Logger) *quiesce.Loop {
|
||||
if cfg.LocalAPI.Endpoint == "" || cfg.LocalAPI.Token == "" {
|
||||
return nil // not a provisioned guest — no agent to back up against
|
||||
}
|
||||
@@ -1692,6 +1700,11 @@ func startQuiesceLoop(ctx context.Context, cfg *config.Config, stackMgr *stacks.
|
||||
StatusPoll: statusPoll,
|
||||
MaxQuiesce: maxQuiesce,
|
||||
Logger: logger,
|
||||
// Window gate (v0.168.0): read the effective window fresh each poll so a customer change takes
|
||||
// effect without restart. Scheduled cycles run only inside [W+2h, W+6h), with the safety valve.
|
||||
WindowStartFn: func() string {
|
||||
return backupwindow.EffectiveWindow(sett.GetBackupWindowStart(), cfg.Backup.DBDumpSchedule)
|
||||
},
|
||||
})
|
||||
loop.Recover() // crash-safety: restart any stacks stranded-down by a mid-quiesce crash
|
||||
go loop.Run(ctx)
|
||||
|
||||
Reference in New Issue
Block a user