From d7ba5aa91e4a6155fd7a5ac8d493c3e9a2d2cb7d Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 15:30:31 +0200 Subject: [PATCH] Compare each node's want with the fingerprint of the build the controller sends it, explaining a difference by the rule that holds the want back or saying it unexplained (issue 1297, subtask B5a; ADR 0528 clause 4) --- .../internal/assign/compare.go | 52 ++++++++++++++++ .../internal/assign/compare_test.go | 59 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 modules/mesh-delivery-assigner/internal/assign/compare.go create mode 100644 modules/mesh-delivery-assigner/internal/assign/compare_test.go diff --git a/modules/mesh-delivery-assigner/internal/assign/compare.go b/modules/mesh-delivery-assigner/internal/assign/compare.go new file mode 100644 index 0000000..952acd3 --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/assign/compare.go @@ -0,0 +1,52 @@ +package assign + +import ( + "fmt" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// Sent is what the controller sends a node of a module: whether it sends it at all (an open assignment), the build its +// assignment names, and that build's fingerprint of the new form (empty when its record names none). +type Sent struct { + Sends bool + Build string + FP string +} + +// Compare is the difference between a node's want for a module and what the controller sends it (ADR 0528 clause 4), +// and whether there is one. want is nil when the assigner wrote none; newest is the module's newest fingerprint and +// hold the rule that holds the node back from it (Rule 0 when none does). A difference is explained by a rule when the +// controller sent the newest fingerprint while that rule holds the node's want back; every other is unexplained, said +// in words, for acceptance test 17 of design 47 to read. +func Compare(want *delivery.Want, sent Sent, newest string, hold Holding) (delivery.Difference, bool) { + var d delivery.Difference + removed := want != nil && want.Removed + if want != nil && !removed { + d.Want = want.FP + } + d.WantRemoved = removed + if !sent.Sends { + if want == nil || removed { + return delivery.Difference{}, false + } + d.Unexplained, d.Words = true, "the node wants "+short(want.FP)+" and the controller sends it nothing" + return d, true + } + d.Sent, d.SentBuild = sent.FP, sent.Build + switch { + case sent.FP == "": + d.Unexplained, d.Words = true, fmt.Sprintf("the build %s the controller sends names no fingerprint of the new form", sent.Build) + case removed: + d.Unexplained, d.Words = true, fmt.Sprintf("the want is removed and the controller still sends %s", short(sent.FP)) + case sent.FP == d.Want: + return delivery.Difference{}, false + case hold.Rule != 0 && sent.FP == newest: + d.Rule, d.Words = hold.Rule, fmt.Sprintf("the controller sent %s while %s", short(sent.FP), hold.Words) + case d.Want == "": + d.Unexplained, d.Words = true, fmt.Sprintf("the controller sends %s and the node has no want", short(sent.FP)) + default: + d.Unexplained, d.Words = true, fmt.Sprintf("the controller sends %s and the node wants %s", short(sent.FP), short(d.Want)) + } + return d, true +} diff --git a/modules/mesh-delivery-assigner/internal/assign/compare_test.go b/modules/mesh-delivery-assigner/internal/assign/compare_test.go new file mode 100644 index 0000000..da4a14f --- /dev/null +++ b/modules/mesh-delivery-assigner/internal/assign/compare_test.go @@ -0,0 +1,59 @@ +package assign + +import ( + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// 1297/B5: given the controller sent F to a node while the canary is not yet healthy on F, the difference names rule 3. +func TestADifferenceExplainedByTheCanary(t *testing.T) { + m, n := ready() + n.Haves["ace"]["mail"] = delivery.Have{FP: fOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK} + out := Wants(m, n, wantedOld(), nil) + want := delivery.Want{FP: fOld, Newest: fNew, Rule: delivery.RuleCanary} + d, ok := Compare(&want, Sent{Sends: true, Build: "b-9", FP: fNew}, fNew, out.Rules["g14"]) + if !ok || d.Rule != delivery.RuleCanary || d.Unexplained || d.Want != fOld || d.Sent != fNew { + t.Fatalf("difference = %+v, %v; want rule 3", d, ok) + } + if _, err := delivery.EncodeDifference(d); err != nil { + t.Errorf("the difference cannot be written: %v", err) + } +} + +// 1297/B5: a want equal to what is sent is no difference; nothing sent and no want is none; every other is written +// unexplained, with words, and is writable. +func TestCompare(t *testing.T) { + w := delivery.Want{FP: fNew} + if _, ok := Compare(&w, Sent{Sends: true, Build: "b", FP: fNew}, fNew, Holding{}); ok { + t.Errorf("an equal want and send is a difference") + } + if _, ok := Compare(nil, Sent{}, fNew, Holding{}); ok { + t.Errorf("no want and nothing sent is a difference") + } + removed := delivery.Want{Removed: true} + if _, ok := Compare(&removed, Sent{}, fNew, Holding{}); ok { + t.Errorf("a removed want and nothing sent is a difference") + } + old := delivery.Want{FP: fOld} + for name, c := range map[string]struct { + want *delivery.Want + sent Sent + }{ + "sends an older one": {&w, Sent{Sends: true, Build: "b", FP: fOld}}, + "names no fingerprint": {&w, Sent{Sends: true, Build: "b"}}, + "sends a removed module": {&removed, Sent{Sends: true, Build: "b", FP: fNew}}, + "sends nothing": {&w, Sent{}}, + "no want": {nil, Sent{Sends: true, Build: "b", FP: fNew}}, + "no rule holds it back": {&old, Sent{Sends: true, Build: "b", FP: fNew}}, + } { + d, ok := Compare(c.want, c.sent, fNew, Holding{}) + if !ok || !d.Unexplained || d.Words == "" { + t.Errorf("%s: difference = %+v, %v; want it unexplained, with words", name, d, ok) + continue + } + if _, err := delivery.EncodeDifference(d); err != nil { + t.Errorf("%s: the difference cannot be written: %v", name, err) + } + } +}