9cf13a3add
gates / gates (push) Successful in 24s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
90 lines
3.9 KiB
Go
90 lines
3.9 KiB
Go
// Package backupwindow holds the pure time arithmetic for the customer-configurable backup window
|
||
// (v0.168.0). ONE setting — the window start W — drives every nightly leg at FIXED offsets so the
|
||
// legs can never be misordered, and never stores a derived time: the DB dump runs at W, the tier-2
|
||
// mirror at W+60m, the off-box copy at W+105m; the whole-guest (PBS/vzdump) cycle is gated to
|
||
// [W+2h, W+6h). Offsets are constants here, never persisted and never surfaced in the UI.
|
||
package backupwindow
|
||
|
||
import "fmt"
|
||
|
||
// DefaultWindow is the last-resort window when neither settings nor controller.yaml supplies one.
|
||
// It equals the historical hardcoded DB-dump time, so an un-configured box behaves exactly as before.
|
||
const DefaultWindow = "02:30"
|
||
|
||
// Fixed leg offsets from the window start W (minutes). NEVER stored, NEVER exposed in the UI —
|
||
// changing spacing/ordering is a code change here, not customer data.
|
||
const (
|
||
tier2OffsetMin = 60 // tier-2 mirror at W+60m
|
||
offboxOffsetMin = 105 // off-box copy at W+105m
|
||
gateStartMin = 120 // whole-guest gate opens at W+2h
|
||
gateEndMin = 360 // whole-guest gate closes (exclusive) at W+6h
|
||
)
|
||
|
||
// UpdateLegStopOffsetMin is W+5h: the automatic update leg starts no step at or after it, and the
|
||
// full-system backup's gate stops waiting for the leg there, so that backup keeps at least one hour of
|
||
// its [W+2h, W+6h) window (`09` §3 decision 20, v0.271.0). Read by stacks.LegDeadline AND the quiesce
|
||
// gate — one constant, so the two can never disagree (TestLegDeadlineAndGateShareW5h).
|
||
const UpdateLegStopOffsetMin = 300
|
||
|
||
// ParseHHMM parses "HH:MM" (24h) into minutes-since-midnight. It rejects anything but a valid
|
||
// hour:minute — the same contract as the scheduler's parseDailyTime, kept here so this package is
|
||
// dependency-free and reusable by the quiesce gate.
|
||
func ParseHHMM(s string) (int, error) {
|
||
var h, m int
|
||
n, err := fmt.Sscanf(s, "%d:%d", &h, &m)
|
||
if err != nil || n != 2 {
|
||
return 0, fmt.Errorf("expected HH:MM format, got %q", s)
|
||
}
|
||
if h < 0 || h > 23 || m < 0 || m > 59 {
|
||
return 0, fmt.Errorf("invalid time %q: hour must be 0-23, minute 0-59", s)
|
||
}
|
||
return h*60 + m, nil
|
||
}
|
||
|
||
// FmtHHMM renders minutes-since-midnight back to "HH:MM", wrapping across midnight (modulo 24h) so
|
||
// derived legs past 23:59 read correctly (e.g. 23:30 + 60m → 00:30).
|
||
func FmtHHMM(minutes int) string {
|
||
minutes = ((minutes % 1440) + 1440) % 1440
|
||
return fmt.Sprintf("%02d:%02d", minutes/60, minutes%60)
|
||
}
|
||
|
||
// Valid reports whether s is a well-formed HH:MM window value (nil error = valid).
|
||
func Valid(s string) error {
|
||
_, err := ParseHHMM(s)
|
||
return err
|
||
}
|
||
|
||
// LegTimes returns the three derived nightly-leg times (db=W, tier2=W+60m, offbox=W+105m),
|
||
// wrap-safe across midnight. On an invalid start it returns three empty strings — callers pass a
|
||
// value already resolved through EffectiveWindow, which never yields an invalid string.
|
||
func LegTimes(start string) (db, tier2, offbox string) {
|
||
m, err := ParseHHMM(start)
|
||
if err != nil {
|
||
return "", "", ""
|
||
}
|
||
return FmtHHMM(m), FmtHHMM(m + tier2OffsetMin), FmtHHMM(m + offboxOffsetMin)
|
||
}
|
||
|
||
// GateWindow returns the whole-guest backup gate bounds [W+2h, W+6h) as HH:MM strings (for the UI
|
||
// "kb. <from>–<to> között" line and the gate-denial log). Empty strings on an invalid start.
|
||
func GateWindow(start string) (from, to string) {
|
||
m, err := ParseHHMM(start)
|
||
if err != nil {
|
||
return "", ""
|
||
}
|
||
return FmtHHMM(m + gateStartMin), FmtHHMM(m + gateEndMin)
|
||
}
|
||
|
||
// EffectiveWindow resolves the active window by precedence: a valid settings value wins over a valid
|
||
// controller.yaml value, which wins over DefaultWindow. An empty or corrupted value simply falls
|
||
// through — so a bad settings string degrades to the yaml default rather than breaking scheduling.
|
||
func EffectiveWindow(settingsVal, yamlVal string) string {
|
||
if Valid(settingsVal) == nil {
|
||
return settingsVal
|
||
}
|
||
if Valid(yamlVal) == nil {
|
||
return yamlVal
|
||
}
|
||
return DefaultWindow
|
||
}
|