From a6c6d936d5a7812367b96347dc6c59fc6f4837ce Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 1 Nov 2026 15:20:26 +0100 Subject: [PATCH] Agree a toolchain by strict majority and name the nodes that sit out (ADR 0584, issue 1578) One build node on another version stopped every build of the language. The agreed toolchain is now the strict majority's; a tie still waits and names the versions. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- go/delivery/toolchains.go | 52 +++++++++++++++++++----- go/delivery/toolchains_test.go | 74 ++++++++++++++++++++-------------- 2 files changed, 86 insertions(+), 40 deletions(-) diff --git a/go/delivery/toolchains.go b/go/delivery/toolchains.go index 4fa2361..ecaebe4 100644 --- a/go/delivery/toolchains.go +++ b/go/delivery/toolchains.go @@ -99,22 +99,47 @@ type NodeToolchain struct { Toolchain } -// AgreedToolchain is the one Toolchain2 every node's entry of a language agrees on, or none and why: "toolchain not -// yet reported" with no entries, and "builders disagree" naming each differing version and recipe with the nodes -// reporting it when two differ (novox/hq ADR 0405 clause 2). The answer does not depend on the entries' order. The -// entries must all be of one language. -func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) { +// SittingOut is a node whose toolchain of a language is not the agreed one: it builds nothing of that language until +// it reports the agreed version (novox/hq ADR 0584 clause 2). +type SittingOut struct { + Node string + Version string + Recipe string +} + +// Agreement is what the nodes' entries of one language come to (novox/hq ADR 0584). Toolchain is the agreed one, set +// when Wait is empty. Wait is why nothing is agreed: "toolchain not yet reported" with no entries, or "builders +// disagree" naming each version and recipe with the nodes reporting it, when no version has a strict majority. +// SittingOut lists, sorted by node, the nodes that report another version than the agreed one. +type Agreement struct { + Toolchain Toolchain2 + SittingOut []SittingOut + Wait string +} + +// ToolchainAgreement is the agreed toolchain of a language: the version and recipe a strict majority of the +// reporting nodes report (novox/hq ADR 0584 clause 1). With no strict majority (two against two) or no entry it +// gives no toolchain and says why in Wait. The answer does not depend on the entries' order. The entries must all be +// of one language. +func ToolchainAgreement(entries []NodeToolchain) Agreement { if len(entries) == 0 { - return Toolchain2{}, ToolchainNoReport + return Agreement{Wait: ToolchainNoReport} } groups := map[Toolchain2][]string{} for _, e := range entries { k := Toolchain2{Language: e.Language, Version: e.Version, Recipe: e.Recipe} groups[k] = append(groups[k], e.Node) } - if len(groups) == 1 { - for k := range groups { - return k, "" + for k, nodes := range groups { + if 2*len(nodes) > len(entries) { + a := Agreement{Toolchain: k} + for _, e := range entries { + if e.Version != k.Version || e.Recipe != k.Recipe { + a.SittingOut = append(a.SittingOut, SittingOut{Node: e.Node, Version: e.Version, Recipe: e.Recipe}) + } + } + sort.Slice(a.SittingOut, func(i, j int) bool { return a.SittingOut[i].Node < a.SittingOut[j].Node }) + return a } } parts := make([]string, 0, len(groups)) @@ -123,5 +148,12 @@ func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) { parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", "))) } sort.Strings(parts) - return Toolchain2{}, "builders disagree: " + strings.Join(parts, "; ") + return Agreement{Wait: "builders disagree: " + strings.Join(parts, "; ")} +} + +// AgreedToolchain is ToolchainAgreement's toolchain, or none and why (its Wait): the version a strict majority of the +// nodes report (novox/hq ADR 0584, which changes ADR 0405 clause 2 and ADR 0477 clause 2). +func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) { + a := ToolchainAgreement(entries) + return a.Toolchain, a.Wait } diff --git a/go/delivery/toolchains_test.go b/go/delivery/toolchains_test.go index 5b19b13..36fa002 100644 --- a/go/delivery/toolchains_test.go +++ b/go/delivery/toolchains_test.go @@ -55,36 +55,50 @@ func TestToolchainKeyAndValue(t *testing.T) { } } -// 1443/AC2 -func TestAgreedToolchain(t *testing.T) { - tc, none := AgreedToolchain([]NodeToolchain{tcEntry("ace", "go", "1.26.1", "3"), tcEntry("novox", "go", "1.26.1", "3")}) - if tc != (Toolchain2{Language: "go", Version: "1.26.1", Recipe: "3"}) || none != "" { - t.Errorf("%+v %q", tc, none) - } -} - -// 1443/AC3 -func TestAgreedToolchainNoReport(t *testing.T) { - tc, none := AgreedToolchain(nil) - if tc != (Toolchain2{}) || none != "toolchain not yet reported" { - t.Errorf("%+v %q", tc, none) - } -} - -// 1443/AC4 -func TestAgreedToolchainDisagree(t *testing.T) { - a, b, c := tcEntry("ace", "go", "1.26.1", "3"), tcEntry("novox", "go", "1.26.2", "3"), tcEntry("g14", "go", "1.26.1", "4") - _, first := AgreedToolchain([]NodeToolchain{a, b, c}) - _, second := AgreedToolchain([]NodeToolchain{c, b, a}) - if first == "" || first != second { - t.Errorf("order changed the answer: %q vs %q", first, second) - } - for _, want := range []string{"builders disagree", "1.26.1", "1.26.2", "ace", "novox", "g14"} { - if !strings.Contains(first, want) { - t.Errorf("%q does not name %q", first, want) +// 1443/AC2-4, ADR 0584: the agreed toolchain is the strict majority's; a minority node sits out; a tie waits. +func TestToolchainAgreement(t *testing.T) { + e := tcEntry + for _, c := range []struct { + name string + in []NodeToolchain + version string + out []SittingOut + wait []string // "" wait means agreed + }{ + {"all agree", []NodeToolchain{e("ace", "go", "1.26.1", "3"), e("novox", "go", "1.26.1", "3")}, "1.26.1", nil, nil}, + {"three against one", []NodeToolchain{e("ace", "typescript", "26.12.1", "3"), e("g14", "typescript", "26.13.0", "3"), e("novox", "typescript", "26.13.0", "3"), e("shanks", "typescript", "26.13.0", "3")}, + "26.13.0", []SittingOut{{"ace", "26.12.1", "3"}}, nil}, + {"two against two", []NodeToolchain{e("ace", "go", "1.26.1", "3"), e("novox", "go", "1.26.2", "3"), e("g14", "go", "1.26.1", "3"), e("shanks", "go", "1.26.2", "3")}, + "", nil, []string{"builders disagree", "1.26.1 recipe 3 on ace, g14", "1.26.2 recipe 3 on novox, shanks"}}, + {"one node alone", []NodeToolchain{e("ace", "go", "1.26.1", "3")}, "1.26.1", nil, nil}, + {"no report", nil, "", nil, []string{ToolchainNoReport}}, + } { + a := ToolchainAgreement(c.in) + rev := make([]NodeToolchain, len(c.in)) + for i, x := range c.in { + rev[len(c.in)-1-i] = x + } + if b := ToolchainAgreement(rev); b.Wait != a.Wait || b.Toolchain != a.Toolchain { + t.Errorf("%s: order changed the answer: %+v vs %+v", c.name, a, b) + } + if a.Toolchain.Version != c.version || len(a.SittingOut) != len(c.out) { + t.Errorf("%s: %+v", c.name, a) + } + for i := range c.out { + if a.SittingOut[i] != c.out[i] { + t.Errorf("%s: sitting out %+v, want %+v", c.name, a.SittingOut, c.out) + } + } + if (len(c.wait) == 0) != (a.Wait == "") { + t.Errorf("%s: wait %q", c.name, a.Wait) + } + for _, w := range c.wait { + if !strings.Contains(a.Wait, w) { + t.Errorf("%s: %q does not name %q", c.name, a.Wait, w) + } + } + if tc, none := AgreedToolchain(c.in); tc != a.Toolchain || none != a.Wait { + t.Errorf("%s: AgreedToolchain %+v %q", c.name, tc, none) } } - if tc, _ := AgreedToolchain([]NodeToolchain{a, b}); tc != (Toolchain2{}) { - t.Errorf("a disagreement gave %+v", tc) - } }