disk-load retires a reading only on its module's key, and measures as soon as its watch catches up (review round 1)

A round no longer retires readings missing from the watched set, which may be empty or partial;
a module's readings are retired when its key of mesh-host_data is deleted or names fewer items.
A round refused before the watch caught up runs once it has, not an hour later. disk_round reads
this machine's key alone, as its grant allows.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 00:28:29 +01:00
parent 357a53d9ac
commit 6fe0ef321d
5 changed files with 56 additions and 36 deletions
+2
View File
@@ -115,5 +115,7 @@ func (w *Watched) Follow(k *Keeper) error {
return nil
})
w.Ready()
// A round refused while the watch caught up runs now, not at the next hour (the delivery's readings within minutes).
go k.RoundIfRefused(context.Background())
return nil
}
+9 -3
View File
@@ -67,6 +67,7 @@ func TestTheItemsComeFromTheEnginesKeysAndARoundSaysHowFarItGot(t *testing.T) {
if r := round(); r.Ended == nil || !strings.Contains(r.Error, "not watched yet") {
t.Fatalf("the round before the watch is kept as %+v", r)
}
refusedAt := round().Started
for _, step := range []struct {
name string
change stdio.StateChange
@@ -88,11 +89,16 @@ func TestTheItemsComeFromTheEnginesKeysAndARoundSaysHowFarItGot(t *testing.T) {
}
w.Ready()
// A round measures every item it was handed; the missing one fails, and the round goes on and says it.
if _, _, err := k.Round(context.Background(), false); err != nil {
t.Fatalf("the round: %v", err)
// The round refused before the watch caught up runs once it has (Watched.Follow), not at the next hour: it measures
// every item it was handed; the missing one fails, and the round goes on and says it.
now = now.Add(time.Minute)
if !k.RoundIfRefused(context.Background()) || k.RoundIfRefused(context.Background()) {
t.Fatal("the round refused before the watch was not run once when the watch caught up")
}
r := round()
if !r.Started.After(refusedAt) {
t.Fatalf("the round's key still says the refused round: %+v", r)
}
if r.Ended == nil || r.Items != 3 || r.Measured != 3 || len(r.Failed) != 1 || r.Failed[0].Item != "pg.wal" ||
!strings.Contains(r.Failed[0].Why, "does not exist") || measured["/srv/pg"] != 1 || measured["/srv/web"] != 1 {
t.Fatalf("the round's key says %+v; measured %v", r, measured)
+34 -22
View File
@@ -13,6 +13,7 @@ import (
"sort"
"strings"
"sync"
"sync/atomic"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
@@ -59,6 +60,19 @@ type Keeper struct {
pmu sync.Mutex
pending map[string]bool
wake chan struct{}
// unwatched says a round was refused because the watch had not caught up (ErrNotWatchedYet): one runs as soon as
// it has (Watched.Follow), not an hour later.
unwatched atomic.Bool
}
// RoundIfRefused runs one round now when the last was refused for a watch not caught up, and says whether it did.
func (k *Keeper) RoundIfRefused(ctx context.Context) bool {
if !k.unwatched.CompareAndSwap(true, false) {
return false
}
_, _, _ = k.Round(ctx, false)
return true
}
// SayEvery is how many items a round measures between two lines saying how far it got.
@@ -120,6 +134,9 @@ func (k *Keeper) Round(ctx context.Context, walks bool) (written, kept int, err
k.Say("a round of measuring the data declared here starts (walks: %v)", walks)
items, err := k.Items(ctx)
if err != nil {
if errors.Is(err, ErrNotWatchedYet) {
k.unwatched.Store(true)
}
r.Error = err.Error()
end()
k.Say("the round measured nothing: %v", err)
@@ -164,13 +181,8 @@ func (k *Keeper) Round(ctx context.Context, walks bool) (written, kept int, err
}
}
}
retired, err := k.retireGone(items)
written += retired
r.Written = written
if err != nil {
failed = append(failed, err.Error())
r.Failed = append(r.Failed, FailedItem{Item: "the readings no longer declared", Why: err.Error()})
}
// A round retires nothing: a reading is retired only when its module's key says so (MeasureModule), never because
// an item is missing from what the watch holds now, which may be empty or partial.
end()
k.Say("the round ended: %d of %d items measured, %d readings written, %d unchanged, %d failed", r.Measured, r.Items,
written, kept, len(r.Failed))
@@ -237,13 +249,13 @@ func (k *Keeper) MeasureModule(ctx context.Context, module string) {
k.Say("%s's data items changed and are not measured: %v", module, err)
return
}
n, err := k.retireGone(module, items[module])
if err != nil {
k.Say("%s's readings of items it no longer declares here are not retired: %v", module, err)
} else if n > 0 {
k.Say("%s no longer declares %d item(s) here: their readings are retired, measured no more", module, n)
}
if len(items[module]) == 0 {
n, err := k.retireGone(items)
if err != nil {
k.Say("%s declares no data item here any more, and its readings are not retired: %v", module, err)
return
}
k.Say("%s declares no data item here any more: %d reading(s) retired, measured no more", module, n)
return
}
written, kept := 0, 0
@@ -268,19 +280,19 @@ func (k *Keeper) MeasureModule(ctx context.Context, module string) {
kept, len(failed), map[bool]string{true: " (" + strings.Join(failed, "; ") + ")", false: ""}[len(failed) > 0])
}
// retireGone marks retired every reading of this node whose item the node no longer declares — its module was unassigned
// there, or is no longer pulled in — with when and why, and keeps it: the record of what the module left behind, until a
// person deletes it (ADR 0233). It is cleared when the item is declared again (measureOne).
func (k *Keeper) retireGone(declared map[string][]catalogue.PlacedDataItem) (int, error) {
// retireGone marks retired every reading of this node of module whose item its key of the engine's state data no longer
// names — the key was deleted (the module was unassigned there, or is no longer pulled in) or names fewer items — with
// when and why, and keeps it: the record of what the module left behind, until a person deletes it (ADR 0233). It is
// asked only on a change of that module's key (novox/hq ADR 0577 clause 2), never from a round. It is cleared when the
// item is declared again (measureOne).
func (k *Keeper) retireGone(module string, declared []catalogue.PlacedDataItem) (int, error) {
keys, err := k.Store.Keys()
if err != nil {
return 0, err
}
declaredNow := map[string]bool{}
for module, its := range declared {
for _, it := range its {
declaredNow[health.ReadingKey(k.Node, module, it.ID)] = true
}
for _, it := range declared {
declaredNow[health.ReadingKey(k.Node, module, it.ID)] = true
}
now := k.Measurer.Now().UTC()
n := 0
@@ -293,7 +305,7 @@ func (k *Keeper) retireGone(declared map[string][]catalogue.PlacedDataItem) (int
continue
}
r, err := health.DecodeReading(raw)
if err != nil || r.Node != k.Node || !r.RetiredAt.IsZero() {
if err != nil || r.Node != k.Node || r.Module != module || !r.RetiredAt.IsZero() {
continue
}
r.RetiredAt = now
@@ -78,11 +78,16 @@ func TestARoundMeasuresTheDeclaredItemsAndWritesWhatChanged(t *testing.T) {
if _, _, err := k.MeasureNow(context.Background(), "pg", "data"); err != nil || walks != 2 {
t.Fatalf("measure now: %v, %d walks", err, walks)
}
// An item no longer declared is kept, retired with when and why; declared again, it is not retired any more.
// A round retires nothing, even when no item is declared (the watched set may be empty or partial); an item its
// module's key no longer names is kept, retired with when and why; declared again, it is not retired any more.
delete(placed, "pg")
if _, _, err := k.Round(context.Background(), false); err != nil {
t.Fatal(err)
}
if kept, _ := health.DecodeReading(store.m["ace.pg.data"]); !kept.RetiredAt.IsZero() {
t.Fatalf("a round retired a reading: %+v", kept)
}
k.MeasureModule(context.Background(), "pg")
gone, _ := health.DecodeReading(store.m["ace.pg.data"])
if gone.RetiredAt.IsZero() || !strings.Contains(gone.RetiredWhy, "no longer declared on ace") || gone.FirstSeen.IsZero() {
t.Fatalf("a reading of an item no longer declared: %+v", gone)
@@ -73,17 +73,12 @@ func ReadingTools(k *Keeper) []stdio.Tool {
{
Name: "disk_round",
Description: "How far this machine's last round of measuring its data items got (novox/hq ADR 0577 clause 3), from " +
"disk-load's state `rounds`, key `<node>`: when it started and ended (no end while it runs), how many items it had, " +
"measured, wrote and kept unchanged, each item that failed and why, and how old the round is; `node` reads " +
"another machine's key. A round that never ends, or fails every item, shows here. (r)",
Input: map[string]any{
"node": map[string]any{"type": "string", "description": "the machine whose last round to read; this one by default"},
},
"disk-load's state `rounds`, this machine's key `<node>`: when it started and ended (no end while it runs), how " +
"many items it had, measured, wrote and kept unchanged, each item that failed and why, and how old the round is. " +
"A round that never ends, or fails every item, shows here. (r)",
Input: map[string]any{},
Run: func(args map[string]any) (any, error) {
node := text(args, "node")
if node == "" {
node = k.Node
}
node := k.Node
if k.Rounds == nil {
return nil, fmt.Errorf("this disk-load keeps no rounds")
}