v0.133.0: a restore-test can never fill a box's disk; leftovers retried on a timer (R-672, R-673)
gates / gates (push) Successful in 13s
gates / gates (push) Successful in 13s
Space preflight before anything is created (uncompressed size from the vzdump log / PBS snapshot, x1.2 + 5 GiB, thin metadata, off the tested guest's pool when another storage is eligible, unknown refuses, reported as a non-pass result). Failed scratch teardown and the stale-lock sweep retried every 10 min (the sweep under the heavy-op gate). A thin pool crossing 90% requests an immediate host report. Six red-proofs. 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:
@@ -6,6 +6,7 @@ import (
|
||||
"log/slog"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
@@ -35,6 +36,45 @@ type Observer struct {
|
||||
host HostReader
|
||||
ops HostOps
|
||||
logger *slog.Logger
|
||||
|
||||
// R-672 (v0.133.0): a thin pool crossing thinPoolAlarmFraction (data OR metadata) requests an
|
||||
// out-of-band host report at once, so the hub's storage-fill alarm sees it in seconds instead of at
|
||||
// the next 15-minute report. Rising edge per pool; re-armed below thinPoolRearmFraction.
|
||||
highMu sync.Mutex
|
||||
high map[string]bool
|
||||
onThinHigh func()
|
||||
}
|
||||
|
||||
const (
|
||||
thinPoolAlarmFraction = 0.90
|
||||
thinPoolRearmFraction = 0.85
|
||||
)
|
||||
|
||||
// SetThinHighTrigger wires the out-of-band report request (main: the storage trigger channel).
|
||||
func (o *Observer) SetThinHighTrigger(f func()) { o.onThinHigh = f }
|
||||
|
||||
// noteThinFill is the edge detector. key separates data from metadata so each has its own edge.
|
||||
func (o *Observer) noteThinFill(key string, frac float64) {
|
||||
o.highMu.Lock()
|
||||
if o.high == nil {
|
||||
o.high = map[string]bool{}
|
||||
}
|
||||
fire := false
|
||||
switch {
|
||||
case frac >= thinPoolAlarmFraction && !o.high[key]:
|
||||
o.high[key] = true
|
||||
fire = true
|
||||
case frac < thinPoolRearmFraction && o.high[key]:
|
||||
o.high[key] = false
|
||||
}
|
||||
o.highMu.Unlock()
|
||||
if fire {
|
||||
o.logger.Error("storage: thin pool crossed 90% — requesting an immediate host report (the hub alarms)",
|
||||
"pool", key, "fraction", frac)
|
||||
if o.onThinHigh != nil {
|
||||
o.onThinHigh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewObserver builds an Observer. host defaults to a ProcHostReader; logger to the
|
||||
@@ -260,6 +300,7 @@ func (o *Observer) build(s proxmox.Storage, mounts []Mount) observed {
|
||||
o.logger.Warn("storage: lvmthin pool data fill is high (a full pool corrupts every guest on it)",
|
||||
"storage", s.Storage, "data_used_fraction", frac)
|
||||
}
|
||||
o.noteThinFill(s.Storage+"/data", frac)
|
||||
}
|
||||
|
||||
// SMART-only device hint (v0.95.0): a dir-storage that lives INSIDE a shared filesystem (the
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
|
||||
)
|
||||
|
||||
// R-672 (v0.133.0): a thin pool crossing 90 % requests an out-of-band host report ONCE, through the
|
||||
// watchdog's own read path (Known — every few seconds), so the hub's storage-fill alarm sees the pool in
|
||||
// seconds, not at the next 15-minute report. Re-armed below 85 %.
|
||||
//
|
||||
// COMPANION RED-PROOF (REPORT): remove the noteThinFill call from the data path → "no report was
|
||||
// requested when the pool crossed 90 %".
|
||||
func TestThinHigh_RequestsOneReportPerCrossing(t *testing.T) {
|
||||
pool := proxmox.Storage{Storage: "local-lvm", Type: "lvmthin", Content: "rootdir,images", Total: 1000, Active: 1}
|
||||
api := &fakeStorageAPI{node: "n", cluster: []proxmox.Storage{pool}}
|
||||
o := NewObserver(api, &fakeHostReader{}, nil, quietLogger())
|
||||
asked := 0
|
||||
o.SetThinHighTrigger(func() { asked++ })
|
||||
for i, used := range []int64{800, 910, 950, 1000, 840, 920} {
|
||||
p := pool
|
||||
p.Used, p.Avail, p.UsedFraction = used, 1000-used, float64(used)/1000
|
||||
api.nodeSt = []proxmox.Storage{p}
|
||||
if _, err := o.Known(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[int]int{0: 0, 1: 1, 2: 1, 3: 1, 4: 1, 5: 2}[i]
|
||||
if asked != want {
|
||||
t.Fatalf("after %d/1000 used: %d report requests, want %d", used, asked, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user