mesh-self-check: D-fill judges every module's bucket and log against its cap from the bus module's nats_streams, with the controller's kind, key and words (issue 1952, ADR 0297 §6)

Its registry row and test rows are already in f7f02abb; this is the probe they call. Each KV_<module>_<name> and
LOG_<module>_<name> stream is said filling at three quarters of its max_bytes, and kept while a finding is written
until under seven tenths. One read of every stream, so the bus module not answering is the probe failing to run, which
clears nothing; bucket-or-log-unread is not raised.
This commit is contained in:
2026-10-29 23:55:50 +01:00
parent 50d46f6e94
commit c2209864ee
@@ -0,0 +1,112 @@
package main
// D-fill (novox/hq ADR 0297 §6, issue 501): a module's bucket or log filling toward its cap is said before it is full.
// Every module's bucket (`KV_<module>_<name>`) and log (`LOG_<module>_<name>`) is read from the bus module's
// nats_streams, which answers each stream's size and its cap (max_bytes, mesh-catalog cab031bf). Said at three
// quarters of its cap, cleared below seven tenths: between the two a finding already written is kept, so one at its
// threshold is not raised and cleared on every run. The kind, key and words are the controller's
// (mesh-controller bucket_log_fill.go).
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/buscompose"
)
// The fill thresholds, in percent of a bucket's or log's cap.
const (
fillRaiseAt = 75
fillClearBelow = 70
)
// fillRow is what nats_streams answers of a stream's size and cap.
type fillRow struct {
Name string `json:"name"`
Bytes uint64 `json:"bytes"`
MaxBytes uint64 `json:"max_bytes"`
}
// bucketOrLog is a module's bucket or log by its stream: the module, `bucket` or `log`, and its local name. False for
// a stream that is no module's bucket or log (the mesh's own streams).
func bucketOrLog(stream string) (module, kind, name string, ok bool) {
var rest string
switch {
case strings.HasPrefix(stream, "KV_"):
kind, rest = "bucket", strings.TrimPrefix(stream, "KV_")
case strings.HasPrefix(stream, buscompose.LogStreamPrefix):
kind, rest = "log", strings.TrimPrefix(stream, buscompose.LogStreamPrefix)
default:
return "", "", "", false
}
module, name, ok = strings.Cut(rest, "_")
if !ok || module == "" || name == "" {
return "", "", "", false
}
return module, kind, name, true
}
// mibWords is a size as a person reads it, in MiB with one decimal.
func mibWords(b uint64) string { return fmt.Sprintf("%.1f MiB", float64(b)/(1024*1024)) }
// fillKey is where a bucket's or log's fill finding is kept.
func fillKey(stream string) string {
return Observation{Subject: Subject{Scope: ScopeBus, ID: stream}, Token: "filling"}.Key()
}
// fillObservation is what one bucket's or log's fill says: a finding at fillRaiseAt or above, and, while its finding
// is written, at fillClearBelow or above too; nothing otherwise, which is what clears it.
func fillObservation(stream string, r fillRow, open bool) (Observation, bool) {
module, kind, name, ok := bucketOrLog(stream)
if !ok || r.MaxBytes == 0 {
return Observation{}, false // no module's, or with no cap: it cannot fill
}
// Compared in bytes, not rounded percent: 74.9% is not 75%.
atRaise := r.Bytes*100 >= r.MaxBytes*fillRaiseAt
aboveClear := r.Bytes*100 >= r.MaxBytes*fillClearBelow
if !atRaise && !(open && aboveClear) {
return Observation{}, false
}
pct := r.Bytes * 100 / r.MaxBytes
return Observation{Kind: "bucket-or-log-filling", Token: "filling", Severity: Warning,
Subject: Subject{Scope: ScopeBus, ID: stream},
Summary: fmt.Sprintf("%s's %s %s holds %s of %s, %d%%: at its cap the bus refuses the module's next write",
module, kind, name, mibWords(r.Bytes), mibWords(r.MaxBytes), pct),
Said: fmt.Sprintf("module %s, %s %s (stream %s): %d of %d bytes, %d%%", module, kind, name, stream, r.Bytes,
r.MaxBytes, pct),
Headline: "A module's bucket or log on the bus is filling up",
Needs: "decide whether to raise its cap or have the module keep less.",
Explanation: "A bucket or log a module keeps on the bus holds three quarters of its cap or more. When it is " +
"full, the bus refuses what the module writes there.",
Resolved: "It has room again"}, true
}
// probeFill is D-fill: every module's bucket and log on the bus judged against its cap. The bus module not answering
// is the probe failing to run, which clears nothing; a finding already written counts as open.
func probeFill(_ context.Context, c *Checker) ([]Observation, error) {
raw, err := c.Ask(StreamsTool, map[string]any{})
if err != nil {
return nil, fmt.Errorf("the bus module did not answer %s: %w", StreamsTool, err)
}
var a struct {
Detail []fillRow `json:"detail"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("the bus module answered %s with something unreadable: %w", StreamsTool, err)
}
written, err := c.written()
if err != nil {
return nil, fmt.Errorf("its own findings cannot be read, so a fill between the thresholds cannot be judged: %w", err)
}
sort.Slice(a.Detail, func(i, j int) bool { return a.Detail[i].Name < a.Detail[j].Name })
var out []Observation
for _, r := range a.Detail {
if o, said := fillObservation(r.Name, r, written.has(fillKey(r.Name))); said {
out = append(out, o)
}
}
return sorted(out), nil
}