From 209ebeee0364769ae0ef9d54b2bd8b5dc36d0ef8 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 08:52:08 +0100 Subject: [PATCH] =?UTF-8?q?The=20bus=20module's=20have=20names=20what=20it?= =?UTF-8?q?s=20server=20runs,=20and=20a=20node's=20have=20what=20its=20age?= =?UTF-8?q?nt=20keeps=20(ADR=200594=20=C2=A74,=20=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/delivery.go | 24 ++++++++++++++++++- go/delivery/delivery_test.go | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 3b72bb7..839f3fd 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -360,6 +360,12 @@ type Have struct { // slot that holds it, so a provider and a consumer of one value on one node each keep their own. Written in Sealed's // place by a node-engine that knows it; Sealed is read only from a have written before. SealedAt Seals `json:"sealed-at,omitempty"` + // Server is, in the have of the bus module, what the bus server runs (novox/hq ADR 0594 §4): the fingerprint whose + // server the node agent last made, or, from the take-over of a server a node's own composition made, that old-form + // build's name (`build-...`). FP is the fingerprint applied: the two differ while a build changed the server and no + // warranted want replaced it yet. Empty on every other have, and on a bus module's have from a node agent that does + // not keep the server. + Server string `json:"server,omitempty"` } // UserListWritten is the bus's user list as the node-engine of the bus node wrote it to the bus module's file, applied @@ -421,8 +427,15 @@ type HaveNode struct { // controller ended may still say `controller` (composedByTheEndedController), which is read and never written. Composes string `json:"composes,omitempty"` ComposesWhy string `json:"composes-why,omitempty"` + // Keeps names what the node agent keeps as it runs on a want without the warrant's mark (novox/hq ADR 0594 §8): + // KeepsBusServer for a node agent that leaves the bus server alone. Written at each start; the assigner writes the + // bus module's want only to a node whose have names it. Empty from a node agent before it. + Keeps []string `json:"keeps,omitempty"` } +// KeepsBusServer is a node agent that keeps the bus server unless a want is warranted (HaveNode.Keeps, ADR 0594 §8). +const KeepsBusServer = "bus-server" + // ComposesOwn is a node applying its own compositions (HaveNode.Composes). const ComposesOwn = "own" @@ -494,6 +507,10 @@ func (h Have) check() error { return err } } + if h.Server != "" && checkFingerprint(h.Server, "the bus server's fingerprint") != nil && + (!strings.HasPrefix(h.Server, "build-") || checkName("bus server's build", h.Server) != nil) { + return fmt.Errorf("delivery: the bus server runs %q, neither a fingerprint nor a build's name", h.Server) + } switch h.Health { case HealthOK, HealthFailed, HealthWaits, HealthUnknown: default: @@ -529,6 +546,11 @@ func (n HaveNode) check() error { default: return fmt.Errorf("delivery: a node composes %q, not %s", n.Composes, ComposesOwn) } + for _, k := range n.Keeps { + if k == "" { + return fmt.Errorf("delivery: a node keeps an empty name") + } + } for group, a := range n.Candidates { if group == "" { return fmt.Errorf("delivery: a node's candidate answer names no group") @@ -615,7 +637,7 @@ func SameHaveNode(a, b HaveNode) bool { sameJSON(a.Strays, b.Strays) && sameJSON(a.Filters, b.Filters) && sameJSON(a.Profile, b.Profile) && (a.Record == nil) == (b.Record == nil) && (a.Record == nil || SameNodeRecord(*a.Record, *b.Record)) && jsonEqual(a.Sealed, b.Sealed) && maps.Equal(a.SealedAt, b.SealedAt) && jsonEqual(a.Candidates, b.Candidates) && a.Composes == b.Composes && - a.ComposesWhy == b.ComposesWhy + a.ComposesWhy == b.ComposesWhy && slices.Equal(a.Keeps, b.Keeps) } // SamePresence says whether two presences are the same value. diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 328fad7..6dbf110 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -282,6 +282,51 @@ func TestRoundTrip(t *testing.T) { t.Error("a list was sealed to no key") } }) + // 774/S2: the bus module's have names the fingerprint applied and what the server runs, a fingerprint or, at the + // take-over, an old-form build's name (ADR 0594 §4); a have written before decodes as it was. + t.Run("have of the bus with its server", func(t *testing.T) { + fpA, fpB := fp2of('a'), fp2of('b') + for _, server := range []string{fpA, "build-1792674595610176744"} { + in := Have{FP: fpB, Server: server, Phase: PhaseRunning, Health: HealthOK} + b, err := EncodeHave(in) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(b), `"server":"`+server+`"`) { + t.Errorf("the have says %s", b) + } + out, err := DecodeHave(b) + in.V = Version + if err != nil || !SameHave(out, in) || SameHave(out, Have{V: Version, FP: fpB, Phase: PhaseRunning, Health: HealthOK}) { + t.Errorf("got %+v, %v", out, err) + } + } + for _, server := range []string{"nats", "build-a.b", "src2:abc"} { + if _, err := EncodeHave(Have{FP: fpB, Server: server, Phase: PhaseRunning, Health: HealthOK}); err == nil { + t.Errorf("a server running %q was encoded", server) + } + } + old, err := DecodeHave([]byte(`{"v":1,"build":"build-17","phase":"running","health":"ok"}`)) + if err != nil || old.Server != "" || !SameHave(old, Have{V: Version, Build: "build-17", Phase: PhaseRunning, Health: HealthOK}) { + t.Errorf("an old have read %+v, %v", old, err) + } + keeper := HaveNode{Composes: ComposesOwn, Keeps: []string{KeepsBusServer}} + b, err := EncodeHaveNode(keeper) + if err != nil || !strings.Contains(string(b), `"keeps":["bus-server"]`) { + t.Fatalf("the keeper's have is %s, %v", b, err) + } + back, err := DecodeHaveNode(b) + keeper.V = Version + if err != nil || !SameHaveNode(back, keeper) || SameHaveNode(back, HaveNode{V: Version, Composes: ComposesOwn}) { + t.Errorf("the keeper came back %+v, %v", back, err) + } + if older, err := DecodeHaveNode([]byte(`{"v":1,"composes":"own"}`)); err != nil || older.Keeps != nil { + t.Errorf("an older node's have read %+v, %v", older, err) + } + if _, err := EncodeHaveNode(HaveNode{Keeps: []string{""}}); err == nil { + t.Error("a node keeping an empty name was encoded") + } + }) t.Run("have node", func(t *testing.T) { in := HaveNode{Windows: json.RawMessage(`{"a":1}`), Busy: json.RawMessage(`true`)} b, err := EncodeHaveNode(in)