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, .", 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 }