From 6fe0ef321d3351245bb1a7d606422cb1093bd4f3 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 00:28:29 +0100 Subject: [PATCH] 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 --- modules/disk-load/cmd/disk-load/data.go | 2 + modules/disk-load/cmd/disk-load/data_test.go | 12 +++- modules/disk-load/cmd/disk-load/keeper.go | 56 +++++++++++-------- .../disk-load/cmd/disk-load/readings_test.go | 7 ++- .../disk-load/cmd/disk-load/readings_tools.go | 15 ++--- 5 files changed, 56 insertions(+), 36 deletions(-) diff --git a/modules/disk-load/cmd/disk-load/data.go b/modules/disk-load/cmd/disk-load/data.go index b7561238..724cec2d 100644 --- a/modules/disk-load/cmd/disk-load/data.go +++ b/modules/disk-load/cmd/disk-load/data.go @@ -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 } diff --git a/modules/disk-load/cmd/disk-load/data_test.go b/modules/disk-load/cmd/disk-load/data_test.go index 2306f159..5d375056 100644 --- a/modules/disk-load/cmd/disk-load/data_test.go +++ b/modules/disk-load/cmd/disk-load/data_test.go @@ -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) diff --git a/modules/disk-load/cmd/disk-load/keeper.go b/modules/disk-load/cmd/disk-load/keeper.go index 5113d8d2..5d9e7a16 100644 --- a/modules/disk-load/cmd/disk-load/keeper.go +++ b/modules/disk-load/cmd/disk-load/keeper.go @@ -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 diff --git a/modules/disk-load/cmd/disk-load/readings_test.go b/modules/disk-load/cmd/disk-load/readings_test.go index d9a7777a..eb1d5785 100644 --- a/modules/disk-load/cmd/disk-load/readings_test.go +++ b/modules/disk-load/cmd/disk-load/readings_test.go @@ -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) diff --git a/modules/disk-load/cmd/disk-load/readings_tools.go b/modules/disk-load/cmd/disk-load/readings_tools.go index 87d3a8f9..044e3d46 100644 --- a/modules/disk-load/cmd/disk-load/readings_tools.go +++ b/modules/disk-load/cmd/disk-load/readings_tools.go @@ -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 ``: 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 ``: 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") }