From 31e90aaa0f0d7749bcbff2d5d7db649fdd2ba967 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 23:56:06 +0100 Subject: [PATCH] Keep when each data item was first seen and when and why it was retired in its reading, so the self-check's D13 judges a new or retired item from disk-load's state (issue 1955, ADR 0571 clause 3) --- go/health/condition_test.go | 16 ++++++++++++++++ go/health/readings.go | 12 +++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/go/health/condition_test.go b/go/health/condition_test.go index 348e62a..f0085b7 100644 --- a/go/health/condition_test.go +++ b/go/health/condition_test.go @@ -89,6 +89,22 @@ func TestTheHealthLayoutsHoldTheKeepersRules(t *testing.T) { } prev = &r } + // The first reading sets FirstSeen and every one after keeps it, as it keeps a retirement; an older reading reads as before. + if first := time.Date(2026, 10, 20, 1, 0, 0, 0, time.UTC); !prev.FirstSeen.Equal(first) { + t.Fatalf("first seen %s, not %s", prev.FirstSeen, first) + } + retired := *prev + retired.RetiredAt, retired.RetiredWhy = at, "its module left ace" + if r, _ := NextReading(&retired, "ace", retired.PlacedDataItem, retired.Measured, at.Add(time.Hour)); !r.RetiredAt.Equal(at) || + r.RetiredWhy != "its module left ace" || !r.FirstSeen.Equal(prev.FirstSeen) { + t.Fatalf("a retired reading moved on as %+v", r) + } + if old, err := DecodeReading([]byte(`{"v":1,"node":"ace","module":"pg","item":"data"}`)); err != nil || !old.FirstSeen.IsZero() { + t.Fatalf("a reading of before read as %+v: %v", old, err) + } + if raw, _ := json.Marshal(Reading{V: ReadingVersion}); strings.Contains(string(raw), "first_seen") || strings.Contains(string(raw), "retired") { + t.Fatalf("zero fields were written: %s", raw) + } if ReadingKey("ace", "pg", "data") != "ace.pg.data" || ReadingsBucket != "disk-load_readings" { t.Fatal("the readings' key or bucket moved") } diff --git a/go/health/readings.go b/go/health/readings.go index 9b8d189..e02c960 100644 --- a/go/health/readings.go +++ b/go/health/readings.go @@ -67,6 +67,12 @@ type Reading struct { Peak *int64 `json:"peak_bytes,omitempty"` PeakAt string `json:"peak_day,omitempty"` Days []DaySize `json:"days,omitempty"` + // FirstSeen is when the item was first measured on this node, kept by every reading after; RetiredAt and RetiredWhy + // say its module left the node, written by whoever retires it and carried on unchanged. Each is left out while zero, + // so a reading written before reads as it did. + FirstSeen time.Time `json:"first_seen,omitzero"` + RetiredAt time.Time `json:"retired_at,omitzero"` + RetiredWhy string `json:"retired_why,omitempty"` } // ReadingKey is the key of an item on a node: `..`. @@ -88,10 +94,14 @@ func DecodeReading(raw []byte) (Reading, error) { // was taken: a reading that did not change is not written. A measurement with no size (an error, a shallow item) carries // the last size's days on, so the peak outlives one failed measurement. func NextReading(prev *Reading, node string, it catalogue.PlacedDataItem, m Measured, now time.Time) (Reading, bool) { - r := Reading{V: ReadingVersion, Node: node, PlacedDataItem: it, Measured: m} + r := Reading{V: ReadingVersion, Node: node, PlacedDataItem: it, Measured: m, FirstSeen: now.UTC()} var days []DaySize if prev != nil { days = prev.Days + r.RetiredAt, r.RetiredWhy = prev.RetiredAt, prev.RetiredWhy + if !prev.FirstSeen.IsZero() { + r.FirstSeen = prev.FirstSeen + } } today := now.UTC().Format("2006-01-02") cut := now.UTC().Add(-PeakWindow).Format("2006-01-02")