Merge pull request 'A node's have says which bus user list it wrote: delivery.Have's user-list field (ADR 0547 bridge, issue 1945)' (#83) from fix/1945-user-list-follows into main

This commit was merged in pull request #83.
This commit is contained in:
2026-10-25 21:16:03 +00:00
2 changed files with 65 additions and 1 deletions
+34 -1
View File
@@ -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
// (`<node>.<module>.<fp>`), 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:<hex>`.
Wanted string `json:"wanted"`
// Digest is the digest of the file on the machine after the write, `sha256:<hex>`; 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:
@@ -470,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 {
+31
View File
@@ -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)
@@ -275,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")