Files
mesh-delivery/sdk/buildstep/toolchains.go
T

80 lines
3.2 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
}