4618169036
gates / gates (push) Failing after 7s
The ticker survives as the EVALUATION interval only. A tier is DUE when its
newest archive that has settled for `settle` (default 24h) has not been proven:
daily tier -> proved daily on yesterday's archive, weekly tier -> weekly on its
own, newborn -> UNKNOWN.
The trap avoided: the literal reading ("newest archive is >= 24h old") is NEVER
true on a daily tier, so it silently switches restore-testing off where it
matters most. Red-proved at 0 runs over 5 simulated days.
- state records WHICH archive was proven; legacy files keep their time and yield
no proven archive (each tier due once after the upgrade, deliberately)
- two knobs replace one: restore_test_eval_interval_seconds (6h, measured) and
restore_test_settle_seconds (24h). The old cadence key keeps its DISABLE
meaning verbatim and now seeds the settle lag, with a start-up WARN.
- due-check runs BEFORE the heavy-op gate (a frequent poll must not make a
starting backup record a failure, F-A1)
- candidate picker skips implausible archives (a phantom would be due forever)
- new read-only --selftest=restore-test-due prints the verdict + its cost
114 lines
3.6 KiB
Go
114 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"testing"
|
|
)
|
|
|
|
// R-86 Scenario I — the seam-discipline test for the due-check.
|
|
//
|
|
// A due-check is worth nothing if the daemon still wires the OLD picker: every unit test in
|
|
// internal/backup would stay green (they inject the seam directly), the scheduler would ask for the
|
|
// newest archive with no settle cutoff, and the per-archive rule would run against a candidate that
|
|
// changes every time a backup lands. That is the same shape as the v0.91.0 inert seam — built,
|
|
// tested, never called — and this repo has shipped it four times.
|
|
//
|
|
// It walks main.go's AST rather than grepping: a commented-out call still satisfies a substring
|
|
// match, and a comment is not a caller.
|
|
func TestMainWiresTheSettleAwareTierPicker(t *testing.T) {
|
|
f := parseMainForWiring(t)
|
|
|
|
var settlePicker, oldPicker, settleWired, evalInterval bool
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
switch node := n.(type) {
|
|
case *ast.SelectorExpr:
|
|
// runner.PickSettledRestoreCandidateOn passed as a value (not called).
|
|
switch node.Sel.Name {
|
|
case "PickSettledRestoreCandidateOn":
|
|
settlePicker = true
|
|
case "PickRestoreCandidateOn":
|
|
oldPicker = true
|
|
}
|
|
case *ast.KeyValueExpr:
|
|
key, ok := node.Key.(*ast.Ident)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if key.Name == "Settle" {
|
|
settleWired = true
|
|
}
|
|
case *ast.CallExpr:
|
|
if sel, ok := node.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "RestoreTestEvalInterval" {
|
|
evalInterval = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if !settlePicker {
|
|
t.Error("main.go never passes runner.PickSettledRestoreCandidateOn as the scheduler's TierPick — " +
|
|
"the due-check would run without a settle cutoff, i.e. against an archive that may still be being written")
|
|
}
|
|
if oldPicker {
|
|
t.Error("main.go still wires the pre-R-86 PickRestoreCandidateOn as a tier picker — " +
|
|
"two pickers means the one under test is not the one running")
|
|
}
|
|
if !settleWired {
|
|
t.Error("main.go never sets SchedulerOptions.Settle — the settle lag would default to 0 in the daemon " +
|
|
"and every freshly-landed archive would be an immediate candidate")
|
|
}
|
|
if !evalInterval {
|
|
t.Error("main.go never calls cfg.Backup.RestoreTestEvalInterval() — the scheduler would be driven by " +
|
|
"the retired cadence knob")
|
|
}
|
|
}
|
|
|
|
// The two R-85 guarantees the due-check must not have quietly dropped: the spec is still built PER
|
|
// RUN, and the shared heavy-operation gate is still handed to the scheduler.
|
|
func TestMainStillWiresTheHeavyOperationGateAndPerRunSpec(t *testing.T) {
|
|
f := parseMainForWiring(t)
|
|
|
|
var inFlightWired, specIsAFunc bool
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
kv, ok := n.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
key, ok := kv.Key.(*ast.Ident)
|
|
if !ok {
|
|
return true
|
|
}
|
|
switch key.Name {
|
|
case "InFlight":
|
|
inFlightWired = true
|
|
case "Spec":
|
|
// A FuncLit means it is evaluated per run; anything else is a frozen value.
|
|
if _, isFunc := kv.Value.(*ast.FuncLit); isFunc {
|
|
specIsAFunc = true
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if !inFlightWired {
|
|
t.Error("main.go no longer hands the scheduler the shared InFlight gate — a restore-test could pull a " +
|
|
"multi-GB archive over the same tunnel an offsite backup is pushing one over (Scenario F)")
|
|
}
|
|
if !specIsAFunc {
|
|
t.Error("SchedulerOptions.Spec is no longer a function literal — a frozen spec is the R-85 defect " +
|
|
"(the tier and its timeout evaluated once at daemon start, forever)")
|
|
}
|
|
}
|
|
|
|
func parseMainForWiring(t *testing.T) *ast.File {
|
|
t.Helper()
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "main.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse main.go: %v", err)
|
|
}
|
|
return f
|
|
}
|