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
160 lines
5.6 KiB
Go
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
|
|
}
|