92 lines
3.6 KiB
Go
92 lines
3.6 KiB
Go
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"
|
|
|
|
// NodeToolchain is one node's entry of a language, as read from a build seat's state toolchains (the entry is Toolchain).
|
|
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 (its language, version and
|
|
// recipe), 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}
|
|
}
|
|
type identity struct{ Language, Version, Recipe string }
|
|
groups := map[identity][]string{}
|
|
for _, e := range entries {
|
|
k := identity{e.Language, e.Version, e.Recipe}
|
|
groups[k] = append(groups[k], e.Node)
|
|
}
|
|
for k, nodes := range groups {
|
|
if 2*len(nodes) > len(entries) {
|
|
a := Agreement{Toolchain: Toolchain{Language: k.Language, Version: k.Version, Recipe: k.Recipe}}
|
|
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
|
|
}
|
|
|
|
// 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("buildstep: %q is not a toolchain key, <node>.<language>", key)
|
|
}
|
|
if _, err := ToolchainKey(node, language); err != nil {
|
|
return "", "", fmt.Errorf("buildstep: %q is not a toolchain key: %w", key, err)
|
|
}
|
|
return node, language, nil
|
|
}
|