Author SHA1 Message Date
jschoubben c8a434b8c6 Assigner: write the bus's user-list revision for a node whose have is of the old form (issue 1945)
At the switch the bus node's have names an old-form build and no composition, so no
revision at the fingerprint it runs could ever exist, and §15 refused the first want:
the bus's user list on novox stood still from 11:23. The revision now names F when the
controller judged F's composition to differ from the declaration the node last applied
only in the user list (ADR 0539 §7, ADR 0540 §9). Any other difference stays the
operator's by §15, and the pass now says so on neither list instead of saying nothing.
2026-10-25 20:44:27 +01:00
2 changed files with 85 additions and 1 deletions
@@ -278,6 +278,10 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st
if need, wait := at.mustAsk(node, asked, hadLive, ran, running.FP, running.Composed == "", d); wait {
if need != nil {
out.Record = append(out.Record, *need)
} else if !hadLive && ran && runs(running) {
// The bus, which the assigner never moves (§15), runs here with no want: it is on neither list until the operator
// writes its want, and the pass says why (issue 1945).
out.Neither[node] = Holding{Rule: delivery.RuleComposed, Words: busMoveWords(m.Name, node, target, d)}
}
continue
}
@@ -443,6 +447,17 @@ func (m Module) mustAsk(node string, before delivery.Want, had, ran bool, runnin
return &RecordNeed{Node: node, Digest: d.Digest, Judgement: d.Judgement, Words: d.Words}, true
}
// busMoveWords say why the bus module's want at fp is not written on node: its composition differs from what the node runs in more
// than the bus's user list, so it is a move of the bus, which the operator writes (ADR 0539 §15).
func busMoveWords(module, node, fp string, d delivery.ComposedDigest) string {
why := string(d.Judgement)
if w := strings.TrimSpace(d.Words); w != "" {
why += ": " + w
}
return fmt.Sprintf("the composition of %s for %s at %s changes more than the bus's user list (judged %s), so it is a move of the bus, "+
"which the assigner never makes: the operator writes its want (ADR 0539 §15)", module, node, short(fp), why)
}
// recorded says whether the module is of policy record: as the controller holds it beside the composition's digest, the
// operator's own choice included (ADR 0539 §14); on a composition made by hand, which carries none, as the manifest derives it.
func (m Module) recorded(d delivery.ComposedDigest) bool {
@@ -456,11 +471,28 @@ func (m Module) recorded(d delivery.ComposedDigest) bool {
// userListRevision is the want that names the bus module's composition at the fingerprint the node runs when that composition
// differs from the one the node runs only in the bus's user list (ComposedDigest.UserListOnly), and whether there is one. A node
// whose want names another fingerprint than the one it runs, or a module that does not provide the bus, has none.
//
// A node whose have is of the old form (from the old node-engine: a build and no fingerprint of the new form, no composition) runs
// no fingerprint a composition is kept at, so its revision names F, the newest fingerprint, when the controller judged F's
// composition to differ from the declaration the node last applied only in the bus's user list (ADR 0539 §7, ADR 0540 §9). Any
// other difference is a move of the bus and stays the operator's (§15). Without this the bus never got its first want after the
// switch, and its user list was never written again (issue 1945: novox's nats from 11:23 on 2026-10-25).
func (m Module) userListRevision(node string, before delivery.Want, had bool, n Nodes) (delivery.Want, bool) {
if !catalogue.ProvidesBus(m.Manifest) {
return delivery.Want{}, false
}
running, ran := n.Haves[node][m.Name]
if ran && oldForm(running) && m.FP != "" && (!had || before.Removed || before.FP == m.FP) {
d, ok := m.composition(node, m.FP)
if !ok || !d.UserListOnly {
return delivery.Want{}, false
}
w := delivery.Want{FP: m.FP, Composed: d.Digest}
if had && !before.Removed && before.Composed == d.Digest {
w.Backup, w.Words = before.Backup, before.Words
}
return w, true
}
if !ran || running.FP == "" || (had && (before.Removed || before.FP != running.FP)) {
return delivery.Want{}, false
}
@@ -475,6 +507,13 @@ func (m Module) userListRevision(node string, before delivery.Want, had bool, n
return w, true
}
// oldForm says whether a have is of the old form: the old node-engine's, naming a build and no fingerprint of the new form, and no
// composition (issue 1945: novox's nats, build-1792674595610176744).
func oldForm(h delivery.Have) bool {
return h.Phase != delivery.PhaseRemoved && h.Composed == "" && (h.FP != "" || h.Build != "") &&
!strings.HasPrefix(h.FP, delivery.SourcePrefix2)
}
// GrantNamesNode says whether a grant of module names its accounts on node: its own `<node>.<module>` or a process's
// `<node>.<module>.<process>`. A grant that names no account (a module that does not speak on the bus) names every node.
func GrantNamesNode(g delivery.ControllerGrant, node, module string) bool {
@@ -498,8 +498,53 @@ func TestAUserListOnlyRevisionOfTheBusIsWrittenAtOnceBeforeTheRecordHold(t *test
t.Errorf("%s: the user-list revision was asked: %+v", name, out.Record)
}
}
revision := m.Composed
// Issue 1945: a have of the old form (a build, no fingerprint of the new form, no composition) runs no fingerprint a composition is
// kept at. The revision then names F when the controller judged F's composition to differ from the declaration the node last
// applied only in the user list; any other difference is a move of the bus, refused by §15 and said on neither list.
for name, c := range map[string]struct {
userListOnly bool
wanted map[string]delivery.Want
want *delivery.Want // nil: no want written
neither bool
}{
"old form, user list only, no want": {userListOnly: true, want: &delivery.Want{FP: fNew, Composed: userList}},
"old form, user list only, a removed want": {userListOnly: true, wanted: map[string]delivery.Want{"ace": {Removed: true}}, want: &delivery.Want{FP: fNew, Composed: userList}},
"old form, user list only, written before": {userListOnly: true, wanted: map[string]delivery.Want{"ace": {FP: fNew, Composed: userList}}},
"old form, a real move of the bus": {neither: true},
} {
m.Manifest.Provides = []catalogue.Offer{{Name: "mesh-bus"}}
m.Composed = func(node, fp string) (delivery.ComposedDigest, bool) {
if fp != fNew {
return delivery.ComposedDigest{}, false
}
if c.userListOnly {
return delivery.ComposedDigest{Digest: userList, Judgement: delivery.JudgedNeither, UserListOnly: true, Against: dig(fOld),
Words: "only the bus's user list changes, against the declaration ace last applied", Policy: delivery.PolicyRecord}, true
}
return delivery.ComposedDigest{Digest: dig(fNew), Judgement: delivery.JudgedNeither, Words: "the node runs no composition of mail",
Policy: delivery.PolicyRecord}, true
}
n.Haves["ace"]["mail"] = delivery.Have{Build: "build-1792674595610176744", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
out := Wants(m, n, c.wanted, nil)
w, ok := out.Wants["ace"]
switch {
case c.want == nil && ok:
t.Errorf("%s: a want was written: %+v", name, w)
case c.want != nil && (!ok || w.FP != c.want.FP || w.Composed != c.want.Composed || w.Removed):
t.Errorf("%s: ace's want = %+v (written %v), want %+v", name, w, ok, *c.want)
}
if len(out.Record) != 0 {
t.Errorf("%s: asked: %+v", name, out.Record)
}
h, on := out.Neither["ace"]
if on != c.neither || (on && (!strings.Contains(h.Words, "§15") || !strings.Contains(h.Words, "more than the bus's user list"))) {
t.Errorf("%s: on neither list %v (%+v), want %v with the words of §15", name, on, h, c.neither)
}
}
// the same flag on a module that is not the bus changes nothing
m.Manifest.Provides = nil
m.Manifest.Provides, m.Composed = nil, revision
n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
if out := Wants(m, n, map[string]delivery.Want{"ace": {FP: fOld, Composed: dig(fOld)}}, nil); out.Wants["ace"].Composed == userList {
t.Errorf("a module that is not the bus was written at once: %+v", out.Wants["ace"])
}