From 1d9c1028896efc0471d86c01b67277c8fd070e51 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 30 Sep 2026 09:28:56 +0200 Subject: [PATCH] A commit has no order, so the mesh says who runs what and claims no newer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit novox/hq 04-ISSUES/087. The version I shipped this morning said "N machine(s) run an older host than another machine does" and worked it out by comparing versions as strings. A host reports its version as a commit. Commits have no order. On the live mesh it named the three machines running the NEWER host as the ones behind: `ced54d4` sorts above `04a27ca` and means nothing. An arbitrary lexicographic result, presented as a fact, about the one thing this was built to make trustworthy. It now reports the split — which machines run which version — and claims no ordering: 4 machine(s) do not all run the same host: 04a27ca g14, novox, shanks ced54d4 ace a host refuses a declaration carrying a field it does not know, whole — so the mesh may send only what every one of these understands. Which of them is newer is not readable from a commit; that needs a version the host reports as ordered More useful as well as more honest: the reader sees who is on which side of the split, which is what decides whether a field can be sent. A report that confidently says the opposite of the truth is worse than one that says less — which is the subject of 04-ISSUES/145, arriving by my own door within an hour of my closing it. --- cmd/mesh-controller/hosts_behind_test.go | 87 ++++++++++++++---------- cmd/mesh-controller/status.go | 80 +++++++++++----------- 2 files changed, 90 insertions(+), 77 deletions(-) diff --git a/cmd/mesh-controller/hosts_behind_test.go b/cmd/mesh-controller/hosts_behind_test.go index dd0fd50..7563493 100644 --- a/cmd/mesh-controller/hosts_behind_test.go +++ b/cmd/mesh-controller/hosts_behind_test.go @@ -1,6 +1,7 @@ package main import ( + "strings" "testing" "github.com/novox/mesh-controller/internal/inventory" @@ -10,53 +11,65 @@ import ( // field is a flag day, and the mesh had no record of which host any machine ran (novox/hq // 04-ISSUES/087). The order was kept by somebody remembering it. -func TestTheMeshNamesEveryMachineOnAnOlderHostThanAnother(t *testing.T) { - older, newest := hostsBehind([]inventory.Node{ - {Name: "anchor", HostVersion: "2026-09-29-0918"}, - {Name: "laptop", HostVersion: "2026-09-29-0113"}, - {Name: "spare", HostVersion: "2026-09-29-0918"}, +func TestTheMeshNamesWhichMachinesRunWhichHost(t *testing.T) { + split := hostSplit([]inventory.Node{ + {Name: "anchor", HostVersion: "04a27ca"}, + {Name: "laptop", HostVersion: "ced54d4"}, + {Name: "spare", HostVersion: "04a27ca"}, }) - if newest != "2026-09-29-0918" { - t.Fatalf("the newest reported host is %q", newest) + if len(split) != 2 { + t.Fatalf("two versions were reported and the split has %d: %v", len(split), split) } - if len(older) != 1 || older[0].Name != "laptop" { - t.Fatalf("the machines behind another are %v, wanted laptop alone", older) + if got := strings.Join(split["04a27ca"], ","); got != "anchor,spare" && got != "spare,anchor" { + t.Fatalf("04a27ca is held by %q", got) + } + if got := strings.Join(split["ced54d4"], ","); got != "laptop" { + t.Fatalf("ced54d4 is held by %q", got) } } -func TestAMachineThatHasNotSaidIsNotCalledBehind(t *testing.T) { - // It may be running anything. Guessing either way is worse than saying it has not said, which - // `node show` does per machine. - older, newest := hostsBehind([]inventory.Node{ - {Name: "anchor", HostVersion: "2026-09-29-0918"}, - {Name: "quiet"}, +func TestTheMeshDoesNotClaimWhichHostIsNewer(t *testing.T) { + // **The fault this replaced.** A host reports its version as a commit, and commits have no order. + // The first version compared them as strings and, on the live mesh, named the three machines + // running the NEWER host as the ones behind: `ced54d4` sorts above `04a27ca` and means nothing. + // + // There is no assertion to make about which is newer, and that is the point — the type says so. + // hostSplit returns who runs what, and nothing that could be read as an ordering. + split := hostSplit([]inventory.Node{ + {Name: "old-but-sorts-high", HostVersion: "ced54d4"}, + {Name: "new-but-sorts-low", HostVersion: "04a27ca"}, }) - if newest != "2026-09-29-0918" { - t.Fatalf("the newest reported host is %q", newest) - } - for _, n := range older { - if n.Name == "quiet" { - t.Fatal("a machine that reported no host version was called behind") + for version, machines := range split { + if len(machines) != 1 { + t.Fatalf("%s is held by %v", version, machines) } } } -func TestAMeshWhereNothingReportedAHostStatesNoDisagreement(t *testing.T) { - // Every machine predating ADR 0141, or a mesh that has heard nothing since the column existed. - // Saying "0 machines behind" would be a claim the mesh cannot make. - older, newest := hostsBehind([]inventory.Node{{Name: "anchor"}, {Name: "laptop"}}) - if len(older) != 0 || newest != "" { - t.Fatalf("a mesh that has been told no host version reported %v / %q", older, newest) +func TestAMachineThatHasNotSaidIsNotAVersion(t *testing.T) { + // It may be running anything. Counting it as a version would invent a disagreement; `node show` + // says per machine that it has not said. + split := hostSplit([]inventory.Node{ + {Name: "anchor", HostVersion: "04a27ca"}, + {Name: "quiet"}, + }) + if split != nil { + t.Fatalf("one reported version and one silence read as a disagreement: %v", split) } } -func TestMachinesAllOnOneHostAreNotBehind(t *testing.T) { - older, _ := hostsBehind([]inventory.Node{ +func TestMachinesAgreeingOnTheirHostAreNotADisagreement(t *testing.T) { + if split := hostSplit([]inventory.Node{ {Name: "anchor", HostVersion: "v2"}, {Name: "laptop", HostVersion: "v2"}, - }) - if len(older) != 0 { - t.Fatalf("machines agreeing on their host were reported as behind: %v", older) + }); split != nil { + t.Fatalf("machines agreeing reported a split: %v", split) + } +} + +func TestAMeshWhereNothingReportedAHostStatesNoDisagreement(t *testing.T) { + if split := hostSplit([]inventory.Node{{Name: "anchor"}, {Name: "laptop"}}); split != nil { + t.Fatalf("a mesh told no host version reported a split: %v", split) } } @@ -72,18 +85,18 @@ func TestAReportedHostVersionIsKeptAndReadBack(t *testing.T) { if record.HostVersion != "" { t.Fatalf("a machine that never reported one has host version %q", record.HostVersion) } - if err := open.inventory.RecordHostVersion(t.Context(), record.ID, "2026-09-30-0214"); err != nil { + if err := open.inventory.RecordHostVersion(t.Context(), record.ID, "ced54d4"); err != nil { t.Fatal(err) } again, err := open.inventory.NodeByName(t.Context(), "anchor") if err != nil { t.Fatal(err) } - if again.HostVersion != "2026-09-30-0214" { + if again.HostVersion != "ced54d4" { t.Fatalf("the reported host version read back as %q", again.HostVersion) } - // An empty report never clears what a machine last said: a bare word that the node is there - // says nothing about its host. + // An empty report never clears what a machine last said: a bare word that the node is there says + // nothing about its host. if err := open.inventory.RecordHostVersion(t.Context(), record.ID, " "); err != nil { t.Fatal(err) } @@ -91,7 +104,7 @@ func TestAReportedHostVersionIsKeptAndReadBack(t *testing.T) { if err != nil { t.Fatal(err) } - if kept.HostVersion != "2026-09-30-0214" { + if kept.HostVersion != "ced54d4" { t.Fatalf("a report carrying no host version cleared what the machine had said: %q", kept.HostVersion) } diff --git a/cmd/mesh-controller/status.go b/cmd/mesh-controller/status.go index 00d003a..34794b2 100644 --- a/cmd/mesh-controller/status.go +++ b/cmd/mesh-controller/status.go @@ -189,23 +189,30 @@ func printStatus(asked answers) error { fmt.Printf("\n `push --behind` sends them\n\n") } - if older, newest := hostsBehind(nodes); len(older) > 0 { + if split := hostSplit(nodes); len(split) > 1 { // **Before a declaration gains a field, every machine has to understand it** (novox/hq // 04-ISSUES/087). A host refuses a declaration carrying a field it does not know, and refuses // it whole, so every new field is a flag day: hosts first, then the controller. The mesh had - // no record of which host any machine ran, so that order was kept by somebody remembering it, - // and a machine that refused for this reason reported a failure with nothing saying why. + // no record of which host any machine ran, so that order was kept by somebody remembering it. // - // Said as disagreement rather than as "out of date", because nothing delivers a host version - // yet (ADR 0141, not built) and so the mesh has no canonical current one. What it can say - // truthfully is that these machines do not all run the same host, and which is newest of the - // ones it has been told about. - fmt.Printf("%d machine(s) run an older host than another machine does:\n", len(older)) - for _, n := range older { - fmt.Printf(" %-12s %s\n", n.Name, orNotReported(n.HostVersion)) + // **Disagreement, and deliberately not "behind".** A host reports its version as a commit, and + // commits have no order — the first version of this said "N machines run an older host" and + // named the three that were newer, because it compared two hashes as strings. What the mesh + // can say truthfully is that the machines do not all run the same host, and which machines + // hold which. Ordering needs a version that is ordered, and that is the host's to report. + versions := make([]string, 0, len(split)) + for v := range split { + versions = append(versions, v) } - fmt.Printf("\n the newest any machine reports is %s. A host refuses a declaration carrying a\n"+ - " field it does not know, whole — so a new field reaches these machines last\n\n", newest) + sort.Strings(versions) + fmt.Printf("%d machine(s) do not all run the same host:\n", len(nodes)) + for _, v := range versions { + sort.Strings(split[v]) + fmt.Printf(" %-12s %s\n", v, strings.Join(split[v], ", ")) + } + fmt.Printf("\n a host refuses a declaration carrying a field it does not know, whole — so the\n" + + " mesh may send only what every one of these understands. Which of them is newer is\n" + + " not readable from a commit; that needs a version the host reports as ordered\n\n") } if len(asked.untaken) > 0 { @@ -419,39 +426,32 @@ func (a answers) well() bool { len(a.waiting) == 0 && len(a.refused) == 0 && a.network == "" && len(a.untaken) == 0 } -// hostsBehind is every machine reporting an older host than the newest any machine reports, and that -// newest version. +// hostSplit is which machines report which host version, for every version more than one machine +// could disagree about. // -// **Disagreement, not staleness.** Nothing delivers a host version yet -// ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) is accepted and not -// built), so the mesh holds no canonical current version and cannot say a machine is behind THE host. -// It can say these machines are behind ANOTHER MACHINE's, which is the fact that matters before a -// declaration gains a field: the oldest host in the mesh is what the mesh may send -// (novox/hq 04-ISSUES/087). +// **It does not say which is newer, because it cannot.** A host reports its version as a commit, and +// commits have no order. The first version of this returned "the machines behind the newest" by +// comparing versions as strings, and on the live mesh it named the three machines running the NEWER +// host as the ones behind — an arbitrary lexicographic result presented as a fact +// (novox/hq 04-ISSUES/087). A report that confidently says the opposite of the truth is worse than one +// that says less, which is the whole subject of 04-ISSUES/145. // -// A machine that has not reported a version is left out rather than called behind. It may be running -// anything, and guessing in either direction is worse than saying it has not said — which `node show` -// does say, per machine. +// So this answers what is checkable: who runs what. The reader sees the split and the mesh claims no +// ordering. Ordering wants an ordered version, and that is the host's to report rather than this +// function's to infer. // -// Versions are compared as strings, which is enough for the timestamps and commits this mesh uses and -// is wrong for a scheme where "10" sorts before "9". Saying so here rather than pretending: when a -// version becomes something ordered, this is the place that has to learn how. -func hostsBehind(nodes []inventory.Node) ([]inventory.Node, string) { - newest := "" +// Machines that have not reported a version are left out entirely: they are not a version, and +// counting them as one would invent a disagreement. `node show` says per machine that it has not said. +func hostSplit(nodes []inventory.Node) map[string][]string { + out := map[string][]string{} for _, n := range nodes { - if n.HostVersion > newest { - newest = n.HostVersion + if n.HostVersion == "" { + continue } + out[n.HostVersion] = append(out[n.HostVersion], n.Name) } - if newest == "" { - return nil, "" // nothing has reported one; there is no disagreement to state + if len(out) < 2 { + return nil // one version, or none reported: nothing to disagree about } - var older []inventory.Node - for _, n := range nodes { - if n.HostVersion != "" && n.HostVersion != newest { - older = append(older, n) - } - } - sort.Slice(older, func(i, j int) bool { return older[i].Name < older[j].Name }) - return older, newest + return out }