Files
mesh-sdk/go/delivery/toolchains.go
T
jschoubben a6c6d936d5 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
2026-11-01 15:20:26 +01:00

160 lines
5.6 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
// ToolchainsState is the name of the state a build module keeps its toolchains in; its bucket is
// `<module>_toolchains`, built as the SDK names a module's state, by ToolchainsBucket (novox/hq ADR 0465 clause 2).
const ToolchainsState = "toolchains"
// ToolchainsBucket is the bucket of a build module's toolchains.
func ToolchainsBucket(module string) string { return module + "_" + ToolchainsState }
// ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain.
const ToolchainNoReport = "toolchain not yet reported"
func checkToolchainLanguage(language string) error {
switch language {
case "go", "typescript":
return nil
}
return fmt.Errorf("delivery: the toolchain language %q is not go or typescript", language)
}
// ToolchainKey is the key of a node's toolchain of one language: `<node>.<language>`. It refuses a node that is not a
// name and a language other than go or typescript.
func ToolchainKey(node, language string) (string, error) {
if err := checkName("node", node); err != nil {
return "", err
}
if err := checkToolchainLanguage(language); err != nil {
return "", err
}
return node + "." + language, nil
}
// ParseToolchainKey is the inverse of ToolchainKey.
func ParseToolchainKey(key string) (node, language string, err error) {
node, language, ok := strings.Cut(key, ".")
if !ok {
return "", "", fmt.Errorf("delivery: %q is not a toolchain key, <node>.<language>", key)
}
if _, err := ToolchainKey(node, language); err != nil {
return "", "", fmt.Errorf("delivery: %q is not a toolchain key: %w", key, err)
}
return node, language, nil
}
// Toolchain is the value of a toolchains key: the version of the language's toolchain installed on the node and the
// build module's recipe version, and when the module saw them.
type Toolchain struct {
V int `json:"v"`
Language string `json:"language"`
Version string `json:"version"`
Recipe string `json:"recipe"`
At time.Time `json:"at"`
}
func (t Toolchain) check() error {
if err := checkToolchainLanguage(t.Language); err != nil {
return err
}
if t.Version == "" || t.Recipe == "" {
return fmt.Errorf("delivery: a %s toolchain needs its version and its recipe", t.Language)
}
return nil
}
// EncodeToolchain is t as JSON at Version; it refuses a language other than go or typescript and an empty version or
// recipe.
func EncodeToolchain(t Toolchain) ([]byte, error) {
if err := t.check(); err != nil {
return nil, err
}
t.V = Version
return json.Marshal(t)
}
// DecodeToolchain reads a toolchains value; it refuses an unknown version (ErrUnknownVersion) and what EncodeToolchain
// refuses.
func DecodeToolchain(data []byte) (Toolchain, error) {
var t Toolchain
if err := decode(data, &t); err != nil {
return Toolchain{}, err
}
if err := t.check(); err != nil {
return Toolchain{}, err
}
return t, nil
}
// NodeToolchain is one node's entry of a language, as read from the toolchains bucket.
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 (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 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)
}
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): 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
}