A commit has no order, so the mesh says who runs what and claims no newer #153
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/inventory"
|
"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
|
// 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.
|
// 04-ISSUES/087). The order was kept by somebody remembering it.
|
||||||
|
|
||||||
func TestTheMeshNamesEveryMachineOnAnOlderHostThanAnother(t *testing.T) {
|
func TestTheMeshNamesWhichMachinesRunWhichHost(t *testing.T) {
|
||||||
older, newest := hostsBehind([]inventory.Node{
|
split := hostSplit([]inventory.Node{
|
||||||
{Name: "anchor", HostVersion: "2026-09-29-0918"},
|
{Name: "anchor", HostVersion: "04a27ca"},
|
||||||
{Name: "laptop", HostVersion: "2026-09-29-0113"},
|
{Name: "laptop", HostVersion: "ced54d4"},
|
||||||
{Name: "spare", HostVersion: "2026-09-29-0918"},
|
{Name: "spare", HostVersion: "04a27ca"},
|
||||||
})
|
})
|
||||||
if newest != "2026-09-29-0918" {
|
if len(split) != 2 {
|
||||||
t.Fatalf("the newest reported host is %q", newest)
|
t.Fatalf("two versions were reported and the split has %d: %v", len(split), split)
|
||||||
}
|
}
|
||||||
if len(older) != 1 || older[0].Name != "laptop" {
|
if got := strings.Join(split["04a27ca"], ","); got != "anchor,spare" && got != "spare,anchor" {
|
||||||
t.Fatalf("the machines behind another are %v, wanted laptop alone", older)
|
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) {
|
func TestTheMeshDoesNotClaimWhichHostIsNewer(t *testing.T) {
|
||||||
// It may be running anything. Guessing either way is worse than saying it has not said, which
|
// **The fault this replaced.** A host reports its version as a commit, and commits have no order.
|
||||||
// `node show` does per machine.
|
// The first version compared them as strings and, on the live mesh, named the three machines
|
||||||
older, newest := hostsBehind([]inventory.Node{
|
// running the NEWER host as the ones behind: `ced54d4` sorts above `04a27ca` and means nothing.
|
||||||
{Name: "anchor", HostVersion: "2026-09-29-0918"},
|
//
|
||||||
|
// 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"},
|
||||||
|
})
|
||||||
|
for version, machines := range split {
|
||||||
|
if len(machines) != 1 {
|
||||||
|
t.Fatalf("%s is held by %v", version, machines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"},
|
{Name: "quiet"},
|
||||||
})
|
})
|
||||||
if newest != "2026-09-29-0918" {
|
if split != nil {
|
||||||
t.Fatalf("the newest reported host is %q", newest)
|
t.Fatalf("one reported version and one silence read as a disagreement: %v", split)
|
||||||
}
|
}
|
||||||
for _, n := range older {
|
|
||||||
if n.Name == "quiet" {
|
|
||||||
t.Fatal("a machine that reported no host version was called behind")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMachinesAgreeingOnTheirHostAreNotADisagreement(t *testing.T) {
|
||||||
|
if split := hostSplit([]inventory.Node{
|
||||||
|
{Name: "anchor", HostVersion: "v2"},
|
||||||
|
{Name: "laptop", HostVersion: "v2"},
|
||||||
|
}); split != nil {
|
||||||
|
t.Fatalf("machines agreeing reported a split: %v", split)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAMeshWhereNothingReportedAHostStatesNoDisagreement(t *testing.T) {
|
func TestAMeshWhereNothingReportedAHostStatesNoDisagreement(t *testing.T) {
|
||||||
// Every machine predating ADR 0141, or a mesh that has heard nothing since the column existed.
|
if split := hostSplit([]inventory.Node{{Name: "anchor"}, {Name: "laptop"}}); split != nil {
|
||||||
// Saying "0 machines behind" would be a claim the mesh cannot make.
|
t.Fatalf("a mesh told no host version reported a split: %v", split)
|
||||||
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 TestMachinesAllOnOneHostAreNotBehind(t *testing.T) {
|
|
||||||
older, _ := hostsBehind([]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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,18 +85,18 @@ func TestAReportedHostVersionIsKeptAndReadBack(t *testing.T) {
|
|||||||
if record.HostVersion != "" {
|
if record.HostVersion != "" {
|
||||||
t.Fatalf("a machine that never reported one has host version %q", 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)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
again, err := open.inventory.NodeByName(t.Context(), "anchor")
|
again, err := open.inventory.NodeByName(t.Context(), "anchor")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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)
|
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
|
// An empty report never clears what a machine last said: a bare word that the node is there says
|
||||||
// says nothing about its host.
|
// nothing about its host.
|
||||||
if err := open.inventory.RecordHostVersion(t.Context(), record.ID, " "); err != nil {
|
if err := open.inventory.RecordHostVersion(t.Context(), record.ID, " "); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -91,7 +104,7 @@ func TestAReportedHostVersionIsKeptAndReadBack(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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",
|
t.Fatalf("a report carrying no host version cleared what the machine had said: %q",
|
||||||
kept.HostVersion)
|
kept.HostVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,23 +189,30 @@ func printStatus(asked answers) error {
|
|||||||
fmt.Printf("\n `push --behind` sends them\n\n")
|
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
|
// **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
|
// 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
|
// 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,
|
// 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.
|
|
||||||
//
|
//
|
||||||
// Said as disagreement rather than as "out of date", because nothing delivers a host version
|
// **Disagreement, and deliberately not "behind".** A host reports its version as a commit, and
|
||||||
// yet (ADR 0141, not built) and so the mesh has no canonical current one. What it can say
|
// commits have no order — the first version of this said "N machines run an older host" and
|
||||||
// truthfully is that these machines do not all run the same host, and which is newest of the
|
// named the three that were newer, because it compared two hashes as strings. What the mesh
|
||||||
// ones it has been told about.
|
// can say truthfully is that the machines do not all run the same host, and which machines
|
||||||
fmt.Printf("%d machine(s) run an older host than another machine does:\n", len(older))
|
// hold which. Ordering needs a version that is ordered, and that is the host's to report.
|
||||||
for _, n := range older {
|
versions := make([]string, 0, len(split))
|
||||||
fmt.Printf(" %-12s %s\n", n.Name, orNotReported(n.HostVersion))
|
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"+
|
sort.Strings(versions)
|
||||||
" field it does not know, whole — so a new field reaches these machines last\n\n", newest)
|
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 {
|
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
|
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
|
// hostSplit is which machines report which host version, for every version more than one machine
|
||||||
// newest version.
|
// could disagree about.
|
||||||
//
|
//
|
||||||
// **Disagreement, not staleness.** Nothing delivers a host version yet
|
// **It does not say which is newer, because it cannot.** A host reports its version as a commit, and
|
||||||
// ([ADR 0141](../../02-DECISIONS/0141-the-host-delivers-its-own-successor.md) is accepted and not
|
// commits have no order. The first version of this returned "the machines behind the newest" by
|
||||||
// built), so the mesh holds no canonical current version and cannot say a machine is behind THE host.
|
// comparing versions as strings, and on the live mesh it named the three machines running the NEWER
|
||||||
// It can say these machines are behind ANOTHER MACHINE's, which is the fact that matters before a
|
// host as the ones behind — an arbitrary lexicographic result presented as a fact
|
||||||
// declaration gains a field: the oldest host in the mesh is what the mesh may send
|
// (novox/hq 04-ISSUES/087). A report that confidently says the opposite of the truth is worse than one
|
||||||
// (novox/hq 04-ISSUES/087).
|
// 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
|
// So this answers what is checkable: who runs what. The reader sees the split and the mesh claims no
|
||||||
// anything, and guessing in either direction is worse than saying it has not said — which `node show`
|
// ordering. Ordering wants an ordered version, and that is the host's to report rather than this
|
||||||
// does say, per machine.
|
// function's to infer.
|
||||||
//
|
//
|
||||||
// Versions are compared as strings, which is enough for the timestamps and commits this mesh uses and
|
// Machines that have not reported a version are left out entirely: they are not a version, and
|
||||||
// is wrong for a scheme where "10" sorts before "9". Saying so here rather than pretending: when a
|
// counting them as one would invent a disagreement. `node show` says per machine that it has not said.
|
||||||
// version becomes something ordered, this is the place that has to learn how.
|
func hostSplit(nodes []inventory.Node) map[string][]string {
|
||||||
func hostsBehind(nodes []inventory.Node) ([]inventory.Node, string) {
|
out := map[string][]string{}
|
||||||
newest := ""
|
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n.HostVersion > newest {
|
if n.HostVersion == "" {
|
||||||
newest = n.HostVersion
|
continue
|
||||||
}
|
}
|
||||||
|
out[n.HostVersion] = append(out[n.HostVersion], n.Name)
|
||||||
}
|
}
|
||||||
if newest == "" {
|
if len(out) < 2 {
|
||||||
return nil, "" // nothing has reported one; there is no disagreement to state
|
return nil // one version, or none reported: nothing to disagree about
|
||||||
}
|
}
|
||||||
var older []inventory.Node
|
return out
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user