feat(disk-health): poll hourly, not 6-hourly — measured, not assumed
gates / gates (push) Successful in 9s

Part 4 was gated on a measurement. On demo-hp (Tier 0) the controller's real
/disks fetch — fetchDisks, the same path the check uses, not the 60s card
cache — costs min 0.805s / median 0.821s / max 0.841s over 10 calls, all HTTP
200, across 3 physical disk rows (2 distinct devices). Median is 6x under the
5s bar, so the <5s branch applies and the interval drops 6h -> 1h.

Why it matters: the one real failing drive's benign excursion lasted about ONE
HOUR and cleared completely. A 6-hourly sampler can land either side of an
excursion like that, see nothing, and then catch the terminal run half a day
late. The smartd history that produced the whole analysis sampled every 30
minutes and only just resolved the shape.
This commit is contained in:
2026-08-14 08:21:55 +02:00
parent c24f1920d9
commit 34d83f5a02
+13 -4
View File
@@ -1401,12 +1401,21 @@ func main() {
stackMgr.RecoverMigration(ctx) stackMgr.RecoverMigration(ctx)
webServer.SetEncryptionKey(encKey) webServer.SetEncryptionKey(encKey)
webServer.SetAppExporter(appExporter) webServer.SetAppExporter(appExporter)
// Disk-health degradation check (v0.169.0): every 6h, compare each physical disk's SMART verdict // Disk-health degradation check (v0.169.0; hourly + persisted state v0.215.0): compare each
// against the in-memory baseline and emit disk_health_degraded on a degradation only (first run // physical disk's SMART verdict against the PERSISTED per-disk record (disk_health_state.go) and
// baselines silently; recovery/UNKNOWN never notify). Only on a provisioned guest (an agent to // emit disk_health_degraded per the v0.215.0 decision rules — escalation against the last ALERTED
// verdict, plus a re-alert for a disk already at Hiba that keeps worsening. A disk's first verdict
// baselines silently; recovery and UNKNOWN never notify. Only on a provisioned guest (an agent to
// read /disks from); the check no-ops gracefully if the agent is unreachable. // read /disks from); the check no-ops gracefully if the agent is unreachable.
//
// WHY HOURLY, not the original 6h: the one real failing drive's benign excursion lasted about ONE
// HOUR (11 Aug 12:28 -> 13:28, cleared completely). A 6-hourly sampler can land either side of an
// excursion like that and see nothing, then catch the terminal run half a day late. Measured on
// demo-hp 2026-08-14, the real /disks fetch costs min 0.805s / median 0.821s / max 0.841s over 10
// calls (3 physical disk rows), so an hourly poll is ~0.02% duty — far under the 5s bar that would
// have kept this at 6h.
if cfg.LocalAPI.Endpoint != "" { if cfg.LocalAPI.Endpoint != "" {
sched.Every("disk-health-check", 6*time.Hour, webServer.RunDiskHealthCheck) sched.Every("disk-health-check", 1*time.Hour, webServer.RunDiskHealthCheck)
} }
// Browser .fab upload (v0.128.0): upload state is in-memory, so a restart strands the .part — // Browser .fab upload (v0.128.0): upload state is in-memory, so a restart strands the .part —
// GC stray part files in every registered drive's exports dir at startup. // GC stray part files in every registered drive's exports dir at startup.