diff --git a/modules/disk-load/cmd/disk-load/keeper.go b/modules/disk-load/cmd/disk-load/keeper.go index ba74a622..388ef136 100644 --- a/modules/disk-load/cmd/disk-load/keeper.go +++ b/modules/disk-load/cmd/disk-load/keeper.go @@ -86,12 +86,55 @@ func (k *Keeper) Round(ctx context.Context, walks bool) (written, kept int, err } } } + retired, err := k.retireGone(items) + written += retired + if err != nil { + failed = append(failed, err.Error()) + } if len(failed) > 0 { return written, kept, errors.New("readings not kept: " + strings.Join(failed, "; ")) } return written, kept, nil } +// 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) { + 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 + } + } + now := k.Measurer.Now().UTC() + n := 0 + for _, key := range keys { + if !strings.HasPrefix(key, k.Node+".") || declaredNow[key] { + continue + } + raw, err := k.Store.Get(key) + if err != nil || len(raw) == 0 { + continue + } + r, err := health.DecodeReading(raw) + if err != nil || r.Node != k.Node || !r.RetiredAt.IsZero() { + continue + } + r.RetiredAt = now + r.RetiredWhy = fmt.Sprintf("no longer declared on %s: its module was unassigned there, or is no longer pulled in", k.Node) + if err := k.Store.Put(key, r); err != nil { + return n, err + } + n++ + } + return n, nil +} + func (k *Keeper) measureOne(ctx context.Context, it catalogue.PlacedDataItem, walks bool) (bool, error) { key := health.ReadingKey(k.Node, it.Module, it.ID) var prev *health.Reading @@ -105,11 +148,15 @@ func (k *Keeper) measureOne(ctx context.Context, it catalogue.PlacedDataItem, wa } } now := k.Measurer.Now() - if prev != nil && it.Measure == catalogue.MeasureWalk && !walks && prev.MeasuredAt != nil && now.Sub(*prev.MeasuredAt) < WalkEvery { + if prev != nil && prev.RetiredAt.IsZero() && it.Measure == catalogue.MeasureWalk && !walks && prev.MeasuredAt != nil && now.Sub(*prev.MeasuredAt) < WalkEvery { return false, nil } m := k.Measurer.Measure(ctx, it) next, changed := health.NextReading(prev, k.Node, it, m, now) + if prev != nil && !prev.RetiredAt.IsZero() { + // Declared again: it is no longer retired. + next.RetiredAt, next.RetiredWhy, changed = time.Time{}, "", true + } if !changed { return false, nil } diff --git a/modules/disk-load/cmd/disk-load/readings_test.go b/modules/disk-load/cmd/disk-load/readings_test.go index e6b23514..d934ad27 100644 --- a/modules/disk-load/cmd/disk-load/readings_test.go +++ b/modules/disk-load/cmd/disk-load/readings_test.go @@ -9,6 +9,7 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/health" ) type memStore struct{ m map[string]json.RawMessage } @@ -65,6 +66,22 @@ 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. + delete(placed, "pg") + if _, _, err := k.Round(context.Background(), false); err != nil { + t.Fatal(err) + } + 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) + } + placed["pg"] = []catalogue.PlacedDataItem{{Module: "pg", ID: "data", Class: "valuable", Path: "/srv/pg", Measure: catalogue.MeasureWalk}} + if _, _, err := k.Round(context.Background(), false); err != nil { + t.Fatal(err) + } + if back, _ := health.DecodeReading(store.m["ace.pg.data"]); !back.RetiredAt.IsZero() { + t.Fatalf("a reading declared again is still retired: %+v", back) + } if _, _, err := k.MeasureNow(context.Background(), "pg", "tmp"); err == nil { t.Fatal("a cache was measured") } diff --git a/modules/disk-load/go.mod b/modules/disk-load/go.mod index 530147e3..638fea42 100644 --- a/modules/disk-load/go.mod +++ b/modules/disk-load/go.mod @@ -2,4 +2,4 @@ module diskload go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029223134-08fc70783a7c +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029225606-31e90aaa0f0d diff --git a/modules/disk-load/go.sum b/modules/disk-load/go.sum index 2d4cc3b2..840941b2 100644 --- a/modules/disk-load/go.sum +++ b/modules/disk-load/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029223134-08fc70783a7c h1:inVTVNIZI4/v7/aVhL+EkKzHCDlN73hJrWKz3jI59mY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029223134-08fc70783a7c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029225606-31e90aaa0f0d h1:HHSXxRS+GEYNgtdg9MFB8W2Y6ycPJF7YLF46y3oj7Ow= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029225606-31e90aaa0f0d/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=