Merge pull request 'A want's self comes from the current placement alone, so a node no longer placed gets the controller's composition instead of a stale self want (issue 1126)' (#86) from fix/1126-self-wants into main
This commit was merged in pull request #86.
This commit is contained in:
@@ -144,6 +144,7 @@ func (p *passer) wants(ctx context.Context) {
|
|||||||
fp := trunk[name].FP2
|
fp := trunk[name].FP2
|
||||||
name := name
|
name := name
|
||||||
m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name],
|
m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name],
|
||||||
|
Self: func(node string) bool { return placed[node] },
|
||||||
Composed: func(node, fp string) (delivery.ComposedDigest, bool) {
|
Composed: func(node, fp string) (delivery.ComposedDigest, bool) {
|
||||||
if placed[node] {
|
if placed[node] {
|
||||||
// The node composes its own (ADR 0547 §2, §4): no composition of the controller's is waited for, the want
|
// The node composes its own (ADR 0547 §2, §4): no composition of the controller's is waited for, the want
|
||||||
@@ -185,7 +186,7 @@ func (p *passer) wants(ctx context.Context) {
|
|||||||
last[node] = p.w.lastHealthy(node, name, fp)
|
last[node] = p.w.lastHealthy(node, name, fp)
|
||||||
}
|
}
|
||||||
out := assign.Wants(m, nodes, wanted[name], last)
|
out := assign.Wants(m, nodes, wanted[name], last)
|
||||||
p.yeses(ctx, name, m, placed, wanted[name], &out)
|
p.yeses(ctx, name, m, wanted[name], &out)
|
||||||
gatherNeither(neither, name, out)
|
gatherNeither(neither, name, out)
|
||||||
leftNeither(left, m, wanted[name], out)
|
leftNeither(left, m, wanted[name], out)
|
||||||
p.sayNoWant(name, m.Nodes, wanted[name], out)
|
p.sayNoWant(name, m.Nodes, wanted[name], out)
|
||||||
@@ -897,9 +898,9 @@ func (p *passer) sayPlacement(differ []assign.Pair) map[string]bool {
|
|||||||
// person must allow (ADR 0547 §3): for a have of phase
|
// person must allow (ADR 0547 §3): for a have of phase
|
||||||
// waiting-yes it raises the record ask once, naming the composition waiting and the node's judgement, and on a yes writes
|
// waiting-yes it raises the record ask once, naming the composition waiting and the node's judgement, and on a yes writes
|
||||||
// the want again naming it; a yes once written is kept while the want stays at its build, so the node is not asked again.
|
// the want again naming it; a yes once written is kept while the want stays at its build, so the node is not asked again.
|
||||||
func (p *passer) yeses(ctx context.Context, module string, m assign.Module, placed map[string]bool, before map[string]delivery.Want, out *assign.Outcome) {
|
func (p *passer) yeses(ctx context.Context, module string, m assign.Module, before map[string]delivery.Want, out *assign.Outcome) {
|
||||||
for _, node := range m.Nodes {
|
for _, node := range m.Nodes {
|
||||||
if !placed[node] {
|
if m.Self == nil || !m.Self(node) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
was, had := before[node]
|
was, had := before[node]
|
||||||
|
|||||||
@@ -47,8 +47,14 @@ type Module struct {
|
|||||||
Now time.Time
|
Now time.Time
|
||||||
// Puts are the puts of the module, by node (ADR 0546): while one owns a node's want (PutOwns), the rules pass over it.
|
// Puts are the puts of the module, by node (ADR 0546): while one owns a node's want (PutOwns), the rules pass over it.
|
||||||
Puts map[string]delivery.Put
|
Puts map[string]delivery.Put
|
||||||
|
// Self says whether node composes the module itself: the catalogue's placement names the node (ADR 0547 §2). It is the one
|
||||||
|
// source of a want's Self: every want the rules write or keep is derived from it (selfDerived), so a want written Self while
|
||||||
|
// the node was placed does not outlive the placement (issue 1126). Nil: no node does.
|
||||||
|
Self func(node string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Module) selfOn(node string) bool { return m.Self != nil && m.Self(node) }
|
||||||
|
|
||||||
func (m Module) composition(node, fp string) (delivery.ComposedDigest, bool) {
|
func (m Module) composition(node, fp string) (delivery.ComposedDigest, bool) {
|
||||||
if m.Composed == nil {
|
if m.Composed == nil {
|
||||||
return delivery.ComposedDigest{}, false
|
return delivery.ComposedDigest{}, false
|
||||||
@@ -418,6 +424,10 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
next, write := m.selfDerived(node, before, had, next, n, &out)
|
||||||
|
if !write {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !had || !delivery.SameWant(normal(before), normal(next)) {
|
if !had || !delivery.SameWant(normal(before), normal(next)) {
|
||||||
out.Wants[node] = next
|
out.Wants[node] = next
|
||||||
}
|
}
|
||||||
@@ -430,6 +440,43 @@ func Wants(m Module, n Nodes, wants map[string]delivery.Want, lastHealthy map[st
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// selfDerived is next with its Self derived from the placement now (Module.Self), whichever rule made it, and whether it is
|
||||||
|
// written. A placed node's want names the build alone (ADR 0547 §2). An unplaced node's want that a rule kept from the want
|
||||||
|
// written while the node was placed (held at F, held back by rule 3 or 5, a data module asked about) names the controller's
|
||||||
|
// composition of the same fingerprint, asked as any move of a record module is (ADR 0539 §14): on 2026-10-27, after the
|
||||||
|
// placement seed's revert, g14's systemd-resolved (held) and forticlient (rule 5), and shanks's forticlient (rule 3), kept
|
||||||
|
// their Self wants, which their node agents, no longer placed, could not compose (issue 1126). With no such composition, or
|
||||||
|
// while its ask waits, the want written stays and the node's holding says why.
|
||||||
|
func (m Module) selfDerived(node string, before delivery.Want, had bool, next delivery.Want, n Nodes, out *Outcome) (delivery.Want, bool) {
|
||||||
|
switch {
|
||||||
|
case next.Removed:
|
||||||
|
return next, true
|
||||||
|
case m.selfOn(node):
|
||||||
|
next.Self, next.Composed = true, ""
|
||||||
|
return next, true
|
||||||
|
case !next.Self && next.Composed != "":
|
||||||
|
return next, true
|
||||||
|
}
|
||||||
|
next.Self, next.Yes = false, ""
|
||||||
|
d, there := m.composition(node, next.FP)
|
||||||
|
if !there || d.Digest == "" {
|
||||||
|
out.Rules[node] = Holding{Rule: delivery.RuleComposed, Words: fmt.Sprintf("%s on %s was composed by the node while it was placed, "+
|
||||||
|
"and the controller has not composed it at %s, so its want stays", m.Name, node, short(next.FP))}
|
||||||
|
return next, false
|
||||||
|
}
|
||||||
|
at := m
|
||||||
|
at.FP = next.FP
|
||||||
|
running, ran := n.Haves[node][m.Name]
|
||||||
|
if need, wait := at.mustAsk(node, before, had, ran, running.FP, running.Composed == "", d); wait {
|
||||||
|
if need != nil && !slices.Contains(out.Record, *need) { // the branch held back at an older fingerprint may have asked it
|
||||||
|
out.Record = append(out.Record, *need)
|
||||||
|
}
|
||||||
|
return next, false
|
||||||
|
}
|
||||||
|
next.Composed = d.Digest
|
||||||
|
return next, true
|
||||||
|
}
|
||||||
|
|
||||||
// NoWantWords say why a node gets no want for a module: the rule that holds its first want back and why (issue 1945).
|
// NoWantWords say why a node gets no want for a module: the rule that holds its first want back and why (issue 1945).
|
||||||
func NoWantWords(rule int, words string) string {
|
func NoWantWords(rule int, words string) string {
|
||||||
return fmt.Sprintf("no want: rule %d: %s", rule, words)
|
return fmt.Sprintf("no want: rule %d: %s", rule, words)
|
||||||
|
|||||||
@@ -87,6 +87,96 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1126/AC-SW1, AC-SW2, AC-SW3: a want written Self while g14 was placed, kept by a rule after the placement is gone, names
|
||||||
|
// the controller's composition of the fingerprint the rule keeps (on 2026-10-27 g14's systemd-resolved, held, and forticlient,
|
||||||
|
// rule 5, kept their Self wants, which g14 could no longer compose); with no such composition, or while a record ask waits, it
|
||||||
|
// stays and is said; a placed node's want is Self with no composition.
|
||||||
|
selfAt := func(fp string) map[string]delivery.Want {
|
||||||
|
return map[string]delivery.Want{"ace": {FP: fNew, Composed: dig(fNew)}, "g14": {FP: fp, Self: true, Yes: dig(fp)}}
|
||||||
|
}
|
||||||
|
atF := func(_ *Module, n *Nodes) {
|
||||||
|
n.Haves["g14"]["mail"] = delivery.Have{FP: fNew, Composed: dig(fNew), Phase: delivery.PhaseFailed, Health: delivery.HealthOK}
|
||||||
|
}
|
||||||
|
for _, c := range []struct {
|
||||||
|
name string
|
||||||
|
set func(*Module, *Nodes)
|
||||||
|
before string
|
||||||
|
want delivery.Want // the want of g14 written; zero: none written
|
||||||
|
rule int // the rule g14's holding names
|
||||||
|
asked bool
|
||||||
|
}{
|
||||||
|
{name: "rule 5 at F", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
|
atF(m, n)
|
||||||
|
n.Haves["ace"]["store"] = delivery.Have{FP: fpOf('s'), Phase: delivery.PhaseRunning, Health: delivery.HealthWaits}
|
||||||
|
}, want: delivery.Want{FP: fNew, Composed: dig(fNew)}, rule: delivery.RuleProviders},
|
||||||
|
{name: "rule 3 at F", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
|
atF(m, n)
|
||||||
|
n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
||||||
|
}, want: delivery.Want{FP: fNew, Composed: dig(fNew)}, rule: delivery.RuleCanary},
|
||||||
|
{name: "rule 3 held back at an older fingerprint", before: fOld, set: func(_ *Module, n *Nodes) {
|
||||||
|
n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}
|
||||||
|
}, want: delivery.Want{FP: fOld, Composed: dig(fOld), Newest: fNew, Rule: delivery.RuleCanary,
|
||||||
|
Words: "the canary ace does not run bbbbbbbbbbbb healthy yet"}, rule: delivery.RuleCanary},
|
||||||
|
{name: "held at F with nothing to set back to", before: fNew, set: func(m *Module, n *Nodes) { atF(m, n); m.Held = true },
|
||||||
|
want: delivery.Want{FP: fNew, Composed: dig(fNew), Words: "mail is held at bbbbbbbbbbbb and g14 ran no other fingerprint healthy to set it back to"},
|
||||||
|
rule: delivery.RuleNotHeld},
|
||||||
|
{name: "held data module", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
|
atF(m, n)
|
||||||
|
m.Held = true
|
||||||
|
m.Manifest.Data = &catalogue.Data{Own: []catalogue.DataItem{{ID: "mail", Class: catalogue.ClassIrreplaceable}}}
|
||||||
|
}, want: delivery.Want{FP: fNew, Composed: dig(fNew),
|
||||||
|
Words: "mail holds data, so it is not set back from bbbbbbbbbbbb: the operator is asked whether to restore it"}, rule: delivery.RuleNotHeld},
|
||||||
|
{name: "a record ask waits", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
|
atF(m, n)
|
||||||
|
recording(m)
|
||||||
|
m.Held = true
|
||||||
|
m.Composed = func(_, fp string) (delivery.ComposedDigest, bool) {
|
||||||
|
return delivery.ComposedDigest{Digest: dig(fp), Judgement: delivery.JudgedRecreates, Against: dig(fOld)}, true
|
||||||
|
}
|
||||||
|
}, rule: delivery.RuleNotHeld, asked: true},
|
||||||
|
{name: "no composition of F", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
|
atF(m, n)
|
||||||
|
m.Composed = func(node, fp string) (delivery.ComposedDigest, bool) {
|
||||||
|
if node == "g14" {
|
||||||
|
return delivery.ComposedDigest{}, false
|
||||||
|
}
|
||||||
|
return composedEverywhere(node, fp)
|
||||||
|
}
|
||||||
|
}, rule: delivery.RuleComposed},
|
||||||
|
{name: "placed", before: fNew, set: func(m *Module, n *Nodes) {
|
||||||
|
atF(m, n)
|
||||||
|
m.Self = func(node string) bool { return node == "g14" }
|
||||||
|
m.Composed = func(node, fp string) (delivery.ComposedDigest, bool) {
|
||||||
|
if node == "g14" {
|
||||||
|
return delivery.ComposedDigest{Judgement: delivery.JudgedNeither}, true
|
||||||
|
}
|
||||||
|
return composedEverywhere(node, fp)
|
||||||
|
}
|
||||||
|
}, want: delivery.Want{FP: fNew, Self: true}},
|
||||||
|
} {
|
||||||
|
m, n := ready()
|
||||||
|
c.set(&m, &n)
|
||||||
|
before := selfAt(c.before)
|
||||||
|
if c.name == "placed" {
|
||||||
|
before["g14"] = delivery.Want{FP: fNew, Composed: dig(fNew)}
|
||||||
|
}
|
||||||
|
out := Wants(m, n, before, nil)
|
||||||
|
if got := out.Wants["g14"]; got != c.want {
|
||||||
|
t.Errorf("%s: g14's want = %+v, want %+v", c.name, got, c.want)
|
||||||
|
}
|
||||||
|
if got := out.Rules["g14"].Rule; got != c.rule {
|
||||||
|
t.Errorf("%s: g14 is held by rule %d (%s), want %d", c.name, got, out.Rules["g14"].Words, c.rule)
|
||||||
|
}
|
||||||
|
if asked := len(out.Record) == 1 && out.Record[0].Node == "g14"; asked != c.asked {
|
||||||
|
t.Errorf("%s: record asks %+v", c.name, out.Record)
|
||||||
|
}
|
||||||
|
if w, ok := out.Wants["g14"]; ok {
|
||||||
|
if _, err := delivery.EncodeWant(w); err != nil {
|
||||||
|
t.Errorf("%s: g14's want does not encode: %v", c.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Rule 6 per node: a grant written for the module's other nodes does not grant a node whose account it does not name
|
// Rule 6 per node: a grant written for the module's other nodes does not grant a node whose account it does not name
|
||||||
// (issue 1945, live on 2026-10-25 at 20:04: ace's mesh-build-go was wanted with no account on the bus).
|
// (issue 1945, live on 2026-10-25 at 20:04: ace's mesh-build-go was wanted with no account on the bus).
|
||||||
m, n := ready()
|
m, n := ready()
|
||||||
|
|||||||
Reference in New Issue
Block a user