v0.297.0: burn-down round 2 — 24 small rows (R-591 R-568 R-567 R-363 R-547 R-10 R-552 R-251 R-104 R-619 R-362 R-675 R-256 R-257 R-240 R-365 R-425 R-565 R-564 R-603 R-454 R-208 R-457-swept) + the banner countdown and deepCopyStack twins; MinAgent 0.131.0
gates / gates (push) Failing after 50s
gates / gates (push) Failing after 50s
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:
@@ -789,7 +789,7 @@ func main() {
|
||||
}
|
||||
if backupMgr != nil && cfg.Offsite.Enabled && cfg.Hub.URL != "" && cfg.Hub.APIKey != "" {
|
||||
bridge := &offsiteapply.Bridge{
|
||||
Cfg: cfg,
|
||||
Cfg: cfg,
|
||||
// Decision 69 (v0.289.0): the box sends its PUBLIC key to the hub's registrar, which pins it
|
||||
// append-only; the box never receives the sub-account password.
|
||||
Registrar: offsiteRegistrar,
|
||||
@@ -1507,13 +1507,18 @@ func main() {
|
||||
// every driveless app's recovery unit) were invisible, and it never reported a free-byte figure or
|
||||
// named a drive.
|
||||
//
|
||||
// CADENCE: DAILY, at 03:30. A fill is a slow-moving quantity — the thing that fills a disk is a
|
||||
// customer's photo library or a nightly backup, not a spike — so a shorter interval buys no
|
||||
// earlier warning and only costs statfs calls. 03:30 is deliberately BEFORE the nightly app-data
|
||||
// legs (db-dump / tier2 / offbox), so a customer who is about to lose a backup to lack of space
|
||||
// hears about it while there is still a night's margin, rather than after the failure.
|
||||
// The interval is NOT a cooldown: repeats are impossible because the check is edge-triggered per
|
||||
// filesystem, and the hub owns cooldown regardless.
|
||||
// CADENCE: every fillWatchInterval (10 min), PLUS the daily 03:30 run, PLUS once after startup.
|
||||
// The original reasoning was "a fill is slow-moving, so daily is enough"; two measurements proved
|
||||
// it wrong. R-363 (2026-08-21): a filesystem filled to 99% just after a daily run and the watcher
|
||||
// said nothing while the backup reserve was already refusing apps. R-547 (2026-09-17): a root
|
||||
// filesystem held at 96% for ten minutes raised no alarm of any kind. A daily sweep cannot carry
|
||||
// the word BEFORE; an interval no longer than the shortest measured window can. The cost is a
|
||||
// statfs per watched filesystem and one summary log line per run.
|
||||
// The interval is NOT a cooldown and adds no repeat mails: the check is edge-triggered against
|
||||
// PERSISTED bands with a hysteresis gap (fillwatch TestWarnsOnceThenIsSilent,
|
||||
// TestThresholdsKeepTheirHysteresisGap), and the hub owns cooldown regardless. The 03:30 run is
|
||||
// kept so the check still lands just before the nightly app-data legs. Pinned by
|
||||
// TestFillWatchRunsOnAnInterval.
|
||||
fillWatcher := fillwatch.New(
|
||||
filepath.Join(cfg.Paths.DataDir, "fillwatch-state.json"), logger,
|
||||
func() []fillwatch.Target { return fillTargets(cfg, sett) },
|
||||
@@ -1544,6 +1549,7 @@ func main() {
|
||||
// space the household can free on the kept-data list. Nothing is deleted by the box (D3).
|
||||
fillWatcher.SetExtra(func(t fillwatch.Target) string { return keptSpaceSentence(stackMgr, t.Path) })
|
||||
sched.Daily("fill-watch", "03:30", func(ctx context.Context) error { return fillWatcher.Check() })
|
||||
sched.Every("fill-watch-interval", fillWatchInterval, func(ctx context.Context) error { return fillWatcher.Check() })
|
||||
|
||||
// AND ONCE SHORTLY AFTER STARTUP. A box that BOOTS with a filesystem already over the line must
|
||||
// warn now, not up to 24 hours later — that is the R-100 shape, a real fault visible only after a
|
||||
@@ -2437,6 +2443,11 @@ func (s gatedAppStopStarter) StartStack(name string) error {
|
||||
// as unreadable (and be skipped, §8.4), never as a filesystem worth warning about.
|
||||
const fillWatchStartupDelay = 90 * time.Second
|
||||
|
||||
// fillWatchInterval is how often the fill check runs between the daily sweeps (R-363, R-547). It must
|
||||
// not exceed the shortest fill window that was measured going unannounced (ten minutes, R-547): any
|
||||
// window at least this long then contains a check. Pinned by TestFillWatchRunsOnAnInterval.
|
||||
const fillWatchInterval = 10 * time.Minute
|
||||
|
||||
// fillTargets is §8.1's watch list: the app-data volume, the system-data volume, and every
|
||||
// registered drive. Resolved at CHECK time, not at startup, so a drive added or decommissioned
|
||||
// between checks is picked up without a controller restart.
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// R-208: `ARG VERSION` / `ARG GIT_COMMIT` declared ABOVE `RUN go mod download` put a per-build value
|
||||
// into that RUN's cache key, so every release re-downloaded the modules (measured: 208 cache records,
|
||||
// every one with usage count 1). The consequence pinned here, in the builder stage: no ARG whose value
|
||||
// changes per build is declared before the module-download step, and both are still declared before
|
||||
// the `go build` that reads them (or the binary would report "dev"/"unknown").
|
||||
func TestR208_DockerfileVersionArgsSitBelowModuleDownload(t *testing.T) {
|
||||
src, err := os.ReadFile("../../Dockerfile")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lines := strings.Split(string(src), "\n")
|
||||
argRe := regexp.MustCompile(`^\s*ARG\s+(VERSION|GIT_COMMIT|BUILD_TIME)\b`)
|
||||
download, build := -1, -1
|
||||
args := map[string]int{}
|
||||
for i, l := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(l), "FROM ") && i > 0 && download >= 0 && build >= 0 {
|
||||
break // the runtime stage: only the builder matters
|
||||
}
|
||||
if m := argRe.FindStringSubmatch(l); m != nil {
|
||||
if _, seen := args[m[1]]; !seen {
|
||||
args[m[1]] = i // the FIRST declaration: any one above the download is the defect
|
||||
}
|
||||
}
|
||||
if strings.Contains(l, "go mod download") && download < 0 {
|
||||
download = i
|
||||
}
|
||||
if strings.Contains(l, "go build") && build < 0 {
|
||||
build = i
|
||||
}
|
||||
}
|
||||
if download < 0 || build < 0 {
|
||||
t.Fatalf("the builder stage no longer has a `go mod download` (%d) or a `go build` (%d) — this test reads the wrong file", download, build)
|
||||
}
|
||||
for _, name := range []string{"VERSION", "GIT_COMMIT"} {
|
||||
at, ok := args[name]
|
||||
if !ok {
|
||||
t.Errorf("ARG %s is not declared in the builder stage — the binary would be built without it", name)
|
||||
continue
|
||||
}
|
||||
if at < download {
|
||||
t.Errorf("R-208: ARG %s (line %d) is declared above `go mod download` (line %d), so every build with a new value re-downloads the modules", name, at+1, download+1)
|
||||
}
|
||||
if at > build {
|
||||
t.Errorf("ARG %s (line %d) is declared after the `go build` that reads it (line %d)", name, at+1, build+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-363 / R-547: the fill watcher ran once a day (plus once after start), so a filesystem that filled
|
||||
// right after the 03:30 run went unannounced for ~24 h, and a disk held at 96 % for ten minutes raised
|
||||
// nothing. The consequence pinned here: main.go registers the SAME fillWatcher.Check on a periodic
|
||||
// job whose interval is no longer than the shortest measured silent window (10 min), so any such
|
||||
// window contains a check. Edge-triggering (no repeat mails at a faster cadence) is pinned in
|
||||
// internal/fillwatch (TestWarnsOnceThenIsSilent, TestEdgeStateSurvivesARestart).
|
||||
func TestFillWatchRunsOnAnInterval(t *testing.T) {
|
||||
const measuredSilentWindow = 10 * time.Minute // R-547, chaos night 2026-09-17
|
||||
if fillWatchInterval <= 0 || fillWatchInterval > measuredSilentWindow {
|
||||
t.Errorf("fillWatchInterval = %s; it must be >0 and <= %s, or a fill window like R-547's can pass unseen", fillWatchInterval, measuredSilentWindow)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "main.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
periodic := 0
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
call, ok := n.(*ast.CallExpr)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
sel, ok := call.Fun.(*ast.SelectorExpr)
|
||||
if !ok || sel.Sel.Name != "Every" || len(call.Args) != 3 {
|
||||
return true
|
||||
}
|
||||
iv, ok := call.Args[1].(*ast.Ident)
|
||||
if !ok || iv.Name != "fillWatchInterval" {
|
||||
return true
|
||||
}
|
||||
// The job body must call fillWatcher.Check — the same edge-triggered check, not a new one.
|
||||
ast.Inspect(call.Args[2], func(m ast.Node) bool {
|
||||
if c, ok := m.(*ast.CallExpr); ok {
|
||||
if s, ok := c.Fun.(*ast.SelectorExpr); ok && s.Sel.Name == "Check" {
|
||||
if x, ok := s.X.(*ast.Ident); ok && x.Name == "fillWatcher" {
|
||||
periodic++
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return true
|
||||
})
|
||||
if periodic != 1 {
|
||||
t.Errorf("R-363: main.go registers fillWatcher.Check on a periodic (sched.Every, fillWatchInterval) job %d times, want 1 — without it the fill check is daily only", periodic)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user