From f63fa86c745663eafd63bc4878e5dbaa81c5278b Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 21:42:13 +0100 Subject: [PATCH 1/2] =?UTF-8?q?Say=20in=20the=20bus=20module's=20have=20th?= =?UTF-8?q?e=20user=20list=20its=20node=20wrote=20alone,=20so=20the=20brid?= =?UTF-8?q?ge=20of=20ADR=200547=20=C2=A78=20reports=20what=20the=20bus=20l?= =?UTF-8?q?oads=20(issue=201945)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/delivery.go | 21 +++++++++++++++++++++ go/delivery/delivery_test.go | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 128c683..769420c 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -331,6 +331,27 @@ type Have struct { Phase Phase `json:"phase"` Health Health `json:"health"` Detail string `json:"detail,omitempty"` + // UserList is what the node-engine of the node holding mesh-broker wrote of the bus's user list, in the have of the + // bus module (novox/hq ADR 0547 §7, §8). Nil on every other have. + UserList *UserListWritten `json:"user-list,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 +// alone and with no want (novox/hq ADR 0547 §8): where it was taken from, what that source holds, and what the file +// holds now. The bus reloads the file itself when its digest changes (the module's entrypoint), so Digest is the list +// the bus loads. Wanted differs from Digest only while a write failed, and Error says why. +type UserListWritten struct { + // Source is the key the list was taken from: the controller's composition of the bus module for this node + // (`..`), or the grants part's `user-list` once it took over. + Source string `json:"source"` + Revision uint64 `json:"revision"` + // Wanted is the digest of the list the source holds, `sha256:`. + Wanted string `json:"wanted"` + // Digest is the digest of the file on the machine after the write, `sha256:`; empty when there is none. + Digest string `json:"digest,omitempty"` + Users int `json:"users"` + At time.Time `json:"at"` + Error string `json:"error,omitempty"` } // HaveNode is a node's own facts, written under HaveNodeKey. Each field is the node-engine's own JSON, carried whole: diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 9d1ab21..3b1ac09 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -3,6 +3,7 @@ package delivery import ( "encoding/json" "errors" + "reflect" "strings" "testing" "time" @@ -149,6 +150,24 @@ func TestRoundTrip(t *testing.T) { t.Fatalf("got %+v want %+v", out, in) } }) + t.Run("have with the user list", func(t *testing.T) { + at := time.Date(2026, 10, 25, 21, 40, 0, 0, time.UTC) + in := Have{FP: "f1", Phase: PhaseRunning, Health: HealthOK, UserList: &UserListWritten{Source: "novox.nats.f1", + Revision: 7, Wanted: "sha256:ab", Digest: "sha256:ab", Users: 93, At: at}} + b, err := EncodeHave(in) + if err != nil { + t.Fatal(err) + } + hasV1(t, b) + out, err := DecodeHave(b) + if err != nil { + t.Fatal(err) + } + in.V = Version + if !reflect.DeepEqual(out, in) || !out.UserList.At.Equal(at) { + t.Fatalf("got %+v want %+v", out, in) + } + }) t.Run("have node", func(t *testing.T) { in := HaveNode{Windows: json.RawMessage(`{"a":1}`), Busy: json.RawMessage(`true`)} b, err := EncodeHaveNode(in) From 0be0ffd6159e7efac1a6a5bc0740bca4f35fecce Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 21:47:30 +0100 Subject: [PATCH 2/2] =?UTF-8?q?Compare=20the=20have's=20user=20list=20by?= =?UTF-8?q?=20what=20it=20says,=20so=20a=20have=20is=20not=20written=20aga?= =?UTF-8?q?in=20for=20the=20same=20list=20(ADR=200547=20=C2=A78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/delivery/delivery.go | 14 +++++++++++++- go/delivery/delivery_test.go | 12 ++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/go/delivery/delivery.go b/go/delivery/delivery.go index 769420c..8fccfaa 100644 --- a/go/delivery/delivery.go +++ b/go/delivery/delivery.go @@ -491,7 +491,19 @@ func DecodePulse(data []byte) (Pulse, error) { } // SameHave says whether two haves are the same value, so a writer writes only when they differ. -func SameHave(a, b Have) bool { return a == b } +func SameHave(a, b Have) bool { + ua, ub := a.UserList, b.UserList + a.UserList, b.UserList = nil, nil + if a != b || (ua == nil) != (ub == nil) { + return false + } + if ua == nil { + return true + } + x, y := *ua, *ub + x.At, y.At = time.Time{}, time.Time{} + return x == y && ua.At.Equal(ub.At) +} // SameHaveNode says whether two node facts are the same value, comparing each carried JSON without its spacing. func SameHaveNode(a, b HaveNode) bool { diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 3b1ac09..a0aba8d 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -294,6 +294,18 @@ func TestSame(t *testing.T) { if SameHave(a, b) { t.Error("different haves same") } + // The user list is compared by what it says, never by where it is held (ADR 0547 §8). + at := time.Date(2026, 10, 25, 21, 0, 0, 0, time.UTC) + ua, ub := a, a + ua.UserList = &UserListWritten{Source: "k", Revision: 1, Wanted: "sha256:a", Digest: "sha256:a", Users: 2, At: at} + ub.UserList = &UserListWritten{Source: "k", Revision: 1, Wanted: "sha256:a", Digest: "sha256:a", Users: 2, At: at.In(time.Local)} + if !SameHave(ua, ub) || SameHave(ua, a) { + t.Error("equal user lists differ, or one said and one not are the same") + } + ub.UserList.Revision = 2 + if SameHave(ua, ub) { + t.Error("different user lists same") + } n := HaveNode{V: 1, Busy: json.RawMessage(`true`)} if !SameHaveNode(n, HaveNode{V: 1, Busy: json.RawMessage(`true`)}) || SameHaveNode(n, HaveNode{V: 1}) { t.Error("have node")