v0.279.0: after_install (decision 45), known default logins on the page, Part D empty-backup alarm, night chain (R-705), R-706
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
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:
@@ -0,0 +1,110 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── R-705 (v0.279.0): "run tonight's chain now" — a debug action ─────────────────────────────────────
|
||||
//
|
||||
// The night runs four legs in ONE order (07 §6.1, 09 §6.4.2): the database/volume dump at W, the
|
||||
// second-drive copy at W+60m, the off-site copy at W+105m, and the automatic update leg chained after the
|
||||
// off-site one. Until now the only way to see that chain by day was to move the backup window and wait
|
||||
// two hours. This runs the same four legs, in the same order, one at a time, NOW. The whole-guest backup
|
||||
// (the agent's) is not part of it — it has no controller trigger (R-705 keeps that half open).
|
||||
//
|
||||
// Refused while any backup/restore op or guarded update runs, and while a chain is already running.
|
||||
|
||||
// nightChain is the four legs; each field is a seam so the order and the refusal are testable without
|
||||
// Docker or restic. production: newNightChain(s).
|
||||
type nightChain struct {
|
||||
dump func(ctx context.Context) error
|
||||
tier2 func()
|
||||
offsite func(ctx context.Context) error // nil: no off-site target on this box
|
||||
leg func(ctx context.Context)
|
||||
busy func() (bool, string)
|
||||
logf func(format string, args ...interface{})
|
||||
}
|
||||
|
||||
var nightChainRunning atomic.Bool
|
||||
|
||||
func (s *Server) newNightChain() nightChain {
|
||||
c := nightChain{
|
||||
dump: s.backupMgr.RunDBDumps,
|
||||
tier2: s.backupMgr.RunAllTier2,
|
||||
leg: func(ctx context.Context) { s.stackMgr.RunUpdateLegNow(ctx, "manual-chain") },
|
||||
logf: s.logger.Printf,
|
||||
busy: func() (bool, string) {
|
||||
if s.backupMgr.IsRunning() || s.backupMgr.RestoreStatus().Running {
|
||||
return true, "a backup or restore is running"
|
||||
}
|
||||
if s.stackMgr.AnyUpdating() {
|
||||
return true, "a guarded update is running"
|
||||
}
|
||||
return false, ""
|
||||
},
|
||||
}
|
||||
if s.backupMgr.OffboxRunnable() {
|
||||
c.offsite = s.backupMgr.RunOffboxBackup
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// start refuses or launches; it reports which legs will run.
|
||||
func (c nightChain) start() (bool, string, []string) {
|
||||
if busy, why := c.busy(); busy {
|
||||
return false, why, nil
|
||||
}
|
||||
if !nightChainRunning.CompareAndSwap(false, true) {
|
||||
return false, "the night's chain is already running", nil
|
||||
}
|
||||
legs := []string{"db-dump", "tier2", "offsite", "update-leg"}
|
||||
if c.offsite == nil {
|
||||
legs = []string{"db-dump", "tier2", "update-leg"}
|
||||
}
|
||||
go c.run()
|
||||
return true, "", legs
|
||||
}
|
||||
|
||||
func (c nightChain) run() {
|
||||
defer nightChainRunning.Store(false)
|
||||
ctx := context.Background()
|
||||
t0 := time.Now()
|
||||
step := func(name string, fn func() error) {
|
||||
s := time.Now()
|
||||
c.logf("[INFO] [night-chain] %s: started", name)
|
||||
if err := fn(); err != nil {
|
||||
c.logf("[WARN] [night-chain] %s: ended with an error after %s: %v — the chain goes on, as the night does", name, time.Since(s).Round(time.Second), err)
|
||||
return
|
||||
}
|
||||
c.logf("[INFO] [night-chain] %s: done in %s", name, time.Since(s).Round(time.Second))
|
||||
}
|
||||
c.logf("[INFO] [night-chain] manual run of tonight's chain: dump → second drive → off-site → update leg")
|
||||
step("db-dump", func() error { return c.dump(ctx) })
|
||||
step("tier2", func() error { c.tier2(); return nil })
|
||||
if c.offsite != nil {
|
||||
step("offsite", func() error { return c.offsite(ctx) })
|
||||
} else {
|
||||
c.logf("[INFO] [night-chain] offsite: no off-site target on this box — skipped, as at night")
|
||||
}
|
||||
step("update-leg", func() error { c.leg(ctx); return nil })
|
||||
c.logf("[INFO] [night-chain] finished in %s", time.Since(t0).Round(time.Second))
|
||||
}
|
||||
|
||||
func (s *Server) debugRunNightChain(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil || s.stackMgr == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Backup manager nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
s.backupMgr.MarkManualRun()
|
||||
ok, why, legs := s.newNightChain().start()
|
||||
if !ok {
|
||||
s.logger.Printf("[WARN] [night-chain] manual run REFUSED: %s", why)
|
||||
writeDebugJSON(w, http.StatusConflict, false, "refused: "+why, nil)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [night-chain] manual run started from %s: %v", r.RemoteAddr, legs)
|
||||
writeDebugJSON(w, http.StatusAccepted, true, "started", map[string]interface{}{"legs": legs})
|
||||
}
|
||||
Reference in New Issue
Block a user