From bc8cb3b153cf0cd99c5b3a6b0bfc037cb0855157 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 23:52:51 +0100 Subject: [PATCH] The bus keeper reads a bare stream's max-age-days "none" as kept for good, as the judge accepts it mesh-knowledge's stream reviews says max-age-days "none", which the indexer's judge accepts (judge/busstate.go noAge), but the keeper refused it ("not a whole number"), so the stream was never made and the keeper stayed unhealthy (2026-11-08, issue 1966). Quick fix; the thorough one, one shared reader of the template options in mesh-sdk catalogue for the judge and the keeper, is noted on issue 1966. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- modules/nats/internal/keeper/keeper_test.go | 5 +++++ modules/nats/internal/keeper/plan.go | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/modules/nats/internal/keeper/keeper_test.go b/modules/nats/internal/keeper/keeper_test.go index c806c9ab1..7c7f3735a 100644 --- a/modules/nats/internal/keeper/keeper_test.go +++ b/modules/nats/internal/keeper/keeper_test.go @@ -216,6 +216,8 @@ func TestEachTemplateIsPlannedUnderTheModulesNames(t *testing.T) { ledger.Types = []catalogue.LedgerType{{Name: "decision", Indexes: []catalogue.LedgerIndex{{Name: "by-topic", On: []string{"topic"}}}}} bare := entry("raw", catalogue.StorageStream, "", nil) bare.Configuration = map[string]json.RawMessage{"when-full": json.RawMessage(`"drop-oldest"`), "max-mib": json.RawMessage("8"), "max-age-days": json.RawMessage("3")} + forGood := entry("reviews", catalogue.StorageStream, "", nil) + forGood.Configuration = map[string]json.RawMessage{"when-full": json.RawMessage(`"refuse"`), "max-mib": json.RawMessage("512"), "max-age-days": json.RawMessage(`"none"`)} for _, row := range []struct { name string entry catalogue.BusStateEntry @@ -247,6 +249,9 @@ func TestEachTemplateIsPlannedUnderTheModulesNames(t *testing.T) { {name: "bare stream", entry: bare, objects: "stream mesh-knowledge_raw [mesh.bus.mesh-knowledge.raw.>] {8 MiB, age 259200, history 0, old, size 0}", binding: "stream mesh-knowledge_raw on mesh.bus.mesh-knowledge.raw"}, + {name: "a bare stream kept for good (max-age-days none, as the judge accepts it)", entry: forGood, + objects: "stream mesh-knowledge_reviews [mesh.bus.mesh-knowledge.reviews.>] {512 MiB, age 0, history 0, new, size 0}", + binding: "stream mesh-knowledge_reviews on mesh.bus.mesh-knowledge.reviews"}, {name: "a local name holding the separator", entry: entry("my_drafts", catalogue.StorageKeyValue, catalogue.TemplateState, nil), refused: `the local name "my_drafts" is empty or holds "_"`}, {name: "objects with no cap", entry: entry("files", catalogue.StorageObjectStore, catalogue.TemplateObjects, nil), diff --git a/modules/nats/internal/keeper/plan.go b/modules/nats/internal/keeper/plan.go index 536ca32b3..419331328 100644 --- a/modules/nats/internal/keeper/plan.go +++ b/modules/nats/internal/keeper/plan.go @@ -226,7 +226,7 @@ func PlanOf(module string, e catalogue.BusStateEntry) (Plan, error) { if err != nil { return Plan{}, err } - age, err := conf.int(catalogue.OptionAgeDays, -1) + age, err := conf.ageDays(-1) if err != nil { return Plan{}, err } @@ -283,6 +283,21 @@ func days(n int64) int64 { return int64((time.Duration(n) * 24 * time.Hour) / time.Second) } +// noAge is how a bare stream says it keeps its messages for good, as the indexer's judge accepts it +// (mesh-delivery modules/mesh-delivery-indexer/judge/busstate.go noAge): kept as no age limit. +const noAge = "none" + +// ageDays is a bare stream's max-age-days: a whole number of days, or "none" for no limit (0). +func (o options) ageDays(fallback int64) (int64, error) { + if raw, ok := o.raw[catalogue.OptionAgeDays]; ok { + var word string + if json.Unmarshal(raw, &word) == nil && word == noAge { + return 0, nil + } + } + return o.int(catalogue.OptionAgeDays, fallback) +} + // options reads a template's options as the manifest wrote them. type options struct{ raw map[string]json.RawMessage } -- 2.54.0