Delivery SDK: the toolchain a language's builders agree on (entries, majority rule) over a seat-neutral form, so the builder, the checker and the indexer read a build seat's toolchains state with one rule (ADR 0634 §1, §2; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// **The toolchain a language's builders agree on** (novox/hq ADR 0584): the nodes report their toolchain in the state
|
||||
// `toolchains` of a node-build seat (the seat's generated type holds the entry); this file holds the one rule over the
|
||||
// entries every reader of that state shares, over a seat-neutral form, so no reader copies it.
|
||||
|
||||
// ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain.
|
||||
const ToolchainNoReport = "toolchain not yet reported"
|
||||
|
||||
// Toolchain is what a toolchain is identified by: the language, the installed version and the build module's recipe
|
||||
// version. It has the fields of the mesh-catalog client's Toolchain2, which a caller converts it to.
|
||||
type Toolchain struct {
|
||||
Language string
|
||||
Version string
|
||||
Recipe string
|
||||
}
|
||||
|
||||
// NodeToolchain is one node's entry of a language, as read from a build seat's state toolchains.
|
||||
type NodeToolchain struct {
|
||||
Node string
|
||||
Toolchain
|
||||
}
|
||||
|
||||
// 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. 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 Toolchain
|
||||
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 Agreement{Wait: ToolchainNoReport}
|
||||
}
|
||||
groups := map[Toolchain][]string{}
|
||||
for _, e := range entries {
|
||||
k := Toolchain{Language: e.Language, Version: e.Version, Recipe: e.Recipe}
|
||||
groups[k] = append(groups[k], e.Node)
|
||||
}
|
||||
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))
|
||||
for k, nodes := range groups {
|
||||
sort.Strings(nodes)
|
||||
parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", ")))
|
||||
}
|
||||
sort.Strings(parts)
|
||||
return Agreement{Wait: "builders disagree: " + strings.Join(parts, "; ")}
|
||||
}
|
||||
|
||||
// AgreedToolchain is ToolchainAgreement's toolchain, or none and why (its Wait).
|
||||
func AgreedToolchain(entries []NodeToolchain) (tc Toolchain, none string) {
|
||||
a := ToolchainAgreement(entries)
|
||||
return a.Toolchain, a.Wait
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package buildstep
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func tcEntry(node, lang, version, recipe string) NodeToolchain {
|
||||
return NodeToolchain{Node: node, Toolchain: Toolchain{Language: lang, Version: version, Recipe: recipe}}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user