v0.294.0: off-site clean-up guard follows the policy's own constants (R-867); no image clean-up while compose pulls (R-863); stderr tail (R-864); move-aside destination logged (R-869)
gates / gates (push) Successful in 28s
gates / gates (push) Successful in 28s
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:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -22,9 +23,14 @@ import (
|
||||
// snapshot for removal. So, refuse when:
|
||||
// - any snapshot is dated in the future (beyond offsiteGuardSkew), or after the hub's newest-allowed
|
||||
// bound (the moment the window opened, plus the same skew);
|
||||
// - the plan would remove a snapshot younger than offsiteGuardMinAge — the honest policy
|
||||
// (--keep-daily 7) never removes the newest snapshot of any of the last 7 days, while a poisoning
|
||||
// shape does exactly that.
|
||||
// - the plan would remove a snapshot whose calendar day is fewer than keepDaily days before today and
|
||||
// that is not superseded the same day — the honest policy (--keep-daily keepDaily) never does that,
|
||||
// while a poisoning shape does exactly that. v0.294.0 (R-867): the line is DERIVED from keepDaily, the
|
||||
// same constant the policy is built from. v0.289–0.293 used a fixed 8-day AGE, which sat inside the
|
||||
// keep window: the snapshot a keep-7-dailies policy drops each night is 7 days + seconds old, so every
|
||||
// window on a box with more than 7 nightly snapshots refused and mailed an error (measured
|
||||
// demo-felhom 2026-10-05). Pinned by TestOffsiteGuard_RealPolicy* (the policy itself, simulated as
|
||||
// restic 0.14.0 applies it, over 15+ nightly snapshots and a month boundary).
|
||||
// And a plan larger than MaxRemove (the hub's number for one week) REFUSES (v0.290.0, per the 2026-10-04
|
||||
// brief, replacing v0.289's cap). The cost, recorded (R-96 rule 4): after a long gap without windows the
|
||||
// honest backlog exceeds a week and the guard refuses until the operator grants a window by hand — R-833.
|
||||
@@ -33,9 +39,14 @@ import (
|
||||
//
|
||||
// Pinned by TestOffsiteGuard_* (offbox_window_test.go), including the lab's 13-fake shape.
|
||||
|
||||
const offsiteGuardSkew = time.Hour
|
||||
|
||||
// The ruled retention policy (SP-2), as constants: BOTH the policy's arguments and the guard's day line
|
||||
// are built from these, so the guard can never sit inside the keep window (R-867).
|
||||
const (
|
||||
offsiteGuardSkew = time.Hour
|
||||
offsiteGuardMinAge = 8 * 24 * time.Hour
|
||||
keepDaily = 7
|
||||
keepWeekly = 4
|
||||
keepMonthly = 6
|
||||
)
|
||||
|
||||
// OffsiteWindow is the hub's answer to "may I prune now?".
|
||||
@@ -67,7 +78,20 @@ type OffsiteWindowClient interface {
|
||||
func (m *Manager) SetOffsiteWindowClient(c OffsiteWindowClient) { m.offsiteWindow = c }
|
||||
|
||||
// retentionPolicy is the ruled policy, unchanged since SP-2 (`--group-by host,tags`).
|
||||
var retentionPolicy = []string{"--group-by", "host,tags", "--keep-daily", "7", "--keep-weekly", "4", "--keep-monthly", "6"}
|
||||
var retentionPolicy = []string{"--group-by", "host,tags",
|
||||
"--keep-daily", strconv.Itoa(keepDaily), "--keep-weekly", strconv.Itoa(keepWeekly), "--keep-monthly", strconv.Itoa(keepMonthly)}
|
||||
|
||||
// calendarDaysBefore: how many calendar days s lies before now, both read in s's own zone — the zone
|
||||
// restic 0.14.0 buckets a snapshot's day in (the offset stored with the snapshot). Edge, recorded: in the
|
||||
// hour after local midnight on a DST change, a box whose snapshots carry two different offsets can read one
|
||||
// day short; the guard then REFUSES (the safe direction) and the next week's window passes.
|
||||
func calendarDaysBefore(s, now time.Time) int {
|
||||
loc := s.Location()
|
||||
a, b := s.In(loc), now.In(loc)
|
||||
da := time.Date(a.Year(), a.Month(), a.Day(), 0, 0, 0, 0, time.UTC)
|
||||
db := time.Date(b.Year(), b.Month(), b.Day(), 0, 0, 0, 0, time.UTC)
|
||||
return int(db.Sub(da).Hours() / 24)
|
||||
}
|
||||
|
||||
type guardSnap struct {
|
||||
ID string `json:"id"`
|
||||
@@ -98,7 +122,8 @@ func supersededSameDay(s guardSnap, all []guardSnap) bool {
|
||||
}
|
||||
|
||||
// offsiteGuard is the PURE decision: from all snapshots and the policy's remove-plan, either the ids to
|
||||
// remove (oldest first) or a refusal reason. v0.290.0 (R-824): a YOUNG snapshot that a newer same-day
|
||||
// remove (oldest first) or a refusal reason. v0.294.0 (R-867): "young" means fewer than keepDaily calendar
|
||||
// days before today, not an age in hours. v0.290.0 (R-824): a YOUNG snapshot that a newer same-day
|
||||
// snapshot of its group supersedes is EXCLUDED (kept for a later window, when it is old) instead of
|
||||
// refusing the run — v0.289.x refused every window after any manual run. A young removal WITHOUT that
|
||||
// explanation still refuses: it is the poisoning signature. Future-dated snapshots, snapshots newer than
|
||||
@@ -114,12 +139,12 @@ func offsiteGuard(all, plan []guardSnap, now, newestAllowed time.Time, maxRemove
|
||||
}
|
||||
var keep []guardSnap
|
||||
for _, s := range plan {
|
||||
if now.Sub(s.Time) < offsiteGuardMinAge {
|
||||
if calendarDaysBefore(s.Time, now) < keepDaily {
|
||||
if supersededSameDay(s, all) {
|
||||
continue // excluded: removed in a later window, once older than offsiteGuardMinAge
|
||||
continue // excluded: removed in a later window, once keepDaily days old
|
||||
}
|
||||
return nil, fmt.Sprintf("the policy would remove snapshot %s from %s — younger than %d days and not superseded the same day, which honest retention never does",
|
||||
s.ShortID, s.Time.UTC().Format(time.RFC3339), int(offsiteGuardMinAge.Hours()/24))
|
||||
return nil, fmt.Sprintf("the policy would remove snapshot %s from %s — within the last %d days kept daily and not superseded the same day, which honest retention never does",
|
||||
s.ShortID, s.Time.UTC().Format(time.RFC3339), keepDaily)
|
||||
}
|
||||
keep = append(keep, s)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user