The hub keeps every machine's sampling rate and history window in step

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-21 10:14:23 -07:00
co-authored by Claude Fable 5.1
parent 668e716601
commit be0f2a5aaf
2 changed files with 59 additions and 5 deletions
+8
View File
@@ -478,6 +478,14 @@ is 11px wide" at 1392); run after, nothing, in both browsers.
9. ✅ **Idle curves vanished**: a core at 10 % rounded to no dots at all in its mini graph.
Anything above zero lights the bottom row, which is also btop's rule.
10. ✅ **Found by the deploy of fix 2 itself: telling the machines once is not enough.** The hub
restarted first and told three nodes that did not yet have the endpoint (404); they then
updated and carried on at 1 s. The same would happen to any node that is off or
restarting when a setting changes. The hub now *keeps* the fleet in step: fifteen seconds
after it starts and every two minutes after, it asks each machine for its sampling rate
and history window and corrects only what differs (logged as `fleet: grid had ms=1000,
now 250`).
## Skins
✅ Top now wears the skin. The ground is the skin's terminal background and every curve and
+51 -5
View File
@@ -113,12 +113,9 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
RefreshMs float64 `json:"refreshMs"`
}
if json.Unmarshal(uiRaw, &ui) == nil && ui.RefreshMs >= 100 {
go s.setSampler(int(ui.RefreshMs), true)
go s.setSampler(int(ui.RefreshMs), false)
}
}
// A node that restarted, or was added since, has its own idea of the window: tell them all.
if histSec > 0 && nm != nil {
go s.pushHistoryWindow(histSec)
go s.keepFleetInStep()
}
m := http.NewServeMux()
@@ -1955,6 +1952,55 @@ func (s *Server) setSampler(ms int, fanOut bool) {
}
}
// keepFleetInStep makes the two fleet-wide settings — the sampling rate and the history window —
// true of every machine, and keeps them true. Telling the machines once, when a setting
// changes, is not enough: a node that was off, restarting or mid-update at that moment never
// hears it (the first deploy of this very feature told three nodes that did not yet know the
// endpoint). So the hub asks each machine what it has, now and every two minutes, and corrects
// only what differs.
func (s *Server) keepFleetInStep() {
for first := true; ; first = false {
if first {
time.Sleep(15 * time.Second) // let a fleet that restarted together come up
} else {
time.Sleep(2 * time.Minute)
}
var ui struct {
RefreshMs float64 `json:"refreshMs"`
}
var histSec int
s.store.View(func(st *store.State) { _ = json.Unmarshal(st.UI, &ui); histSec = st.HistorySec })
want := map[string][2]any{} // path → {field, value}
if ui.RefreshMs >= 100 {
ms := int(ui.RefreshMs)
if hi := int(metrics.MaxInterval.Milliseconds()); ms > hi {
ms = hi
}
want["/api/prefs/sampler"] = [2]any{"ms", ms}
}
if histSec > 0 {
want["/api/prefs/history"] = [2]any{"seconds", histSec}
}
hdr := http.Header{"X-Hamlab-From-Hub": []string{"1"}, "Content-Type": []string{"application/json"}}
for _, name := range s.nodes.Names() {
for path, fv := range want {
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
code, b, err := s.nodes.Do(ctx, name, "GET", path, nil, hdr)
var have map[string]any
if err == nil && code == 200 && json.Unmarshal(b, &have) == nil {
if got, _ := have[fv[0].(string)].(float64); int(got) != fv[1].(int) {
body, _ := json.Marshal(map[string]any{fv[0].(string): fv[1]})
if c, _, e := s.nodes.Do(ctx, name, "PUT", path, body, hdr); e == nil && c < 400 {
log.Printf("fleet: %s had %s=%v, now %v", name, fv[0], got, fv[1])
}
}
}
cancel()
}
}
}
}
func (s *Server) getSampler(w http.ResponseWriter, _ *http.Request) {
jsonOK(w, 200, map[string]any{"ms": s.metrics.Interval().Milliseconds(), "min_ms": metrics.MinInterval.Milliseconds(), "max_ms": metrics.MaxInterval.Milliseconds()})
}