278 lines
11 KiB
Go
278 lines
11 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// **Who holds which seat, and the builder's log** (novox/hq ADR 0571 clauses 1 and 4, issue 1955).
|
|
|
|
// SeatHoldersState is the state the seat mesh-placement's holder writes of who holds which seat on which node (clause 4):
|
|
// one key per seat held once for the mesh, `<seat>`, and one per node of a seat held on every node, `<seat>.<node>`.
|
|
const (
|
|
SeatHoldersState = "seat-holders"
|
|
SeatHoldersBucket = AssignerModule + "_" + SeatHoldersState
|
|
SeatHoldersRead = AssignerModule + "." + SeatHoldersState
|
|
)
|
|
|
|
// SeatHolder is one module holding a seat on a node.
|
|
type SeatHolder struct {
|
|
Module string `json:"module"`
|
|
Node string `json:"node"`
|
|
}
|
|
|
|
// SeatHolders is one key of seat-holders: the seat, its scope, the node for a node seat's key, and its holders
|
|
// (several only for a replicated mesh seat), sorted. Outside says the claim names no seat of the set (catalogue.Seats):
|
|
// a module registered before the set closed, or built outside the indexed repositories, keeps its claim listed, as the
|
|
// controller's `seats` listed it under outside.
|
|
type SeatHolders struct {
|
|
V int `json:"v"`
|
|
Seat string `json:"seat"`
|
|
Scope string `json:"scope"`
|
|
Node string `json:"node,omitempty"`
|
|
Outside bool `json:"outside,omitempty"`
|
|
Holders []SeatHolder `json:"holders"`
|
|
}
|
|
|
|
// SeatHoldersKey is a seat's key: `<seat>` for a seat held once for the mesh (node empty), `<seat>.<node>` for a node
|
|
// seat on one node.
|
|
func SeatHoldersKey(seat, node string) (string, error) {
|
|
if err := checkName("seat", seat); err != nil {
|
|
return "", err
|
|
}
|
|
if node == "" {
|
|
return seat, nil
|
|
}
|
|
if err := checkName("node", node); err != nil {
|
|
return "", err
|
|
}
|
|
return seat + "." + node, nil
|
|
}
|
|
|
|
// SeatHoldersOf is the state composed from every node's placement (ADR 0567 §1): each holding by the seat it claims,
|
|
// keyed as SeatHoldersKey says, with the module holding it and its node. A claim naming a seat's former name is keyed by
|
|
// the seat's name (ADR 0122); one naming no seat is kept, marked Outside. Deterministic, so a writer writes only a key
|
|
// that differs.
|
|
func SeatHoldersOf(placements map[string]Placement) map[string]SeatHolders {
|
|
out := map[string]SeatHolders{}
|
|
nodes := make([]string, 0, len(placements))
|
|
for n := range placements {
|
|
nodes = append(nodes, n)
|
|
}
|
|
sort.Strings(nodes)
|
|
for _, n := range nodes {
|
|
for _, h := range placements[n].Holds {
|
|
node := ""
|
|
if h.Scope == "node" {
|
|
node = n
|
|
}
|
|
seat, known := catalogue.SeatNamed(h.Claim)
|
|
name := h.Claim
|
|
if known {
|
|
name = seat.Name
|
|
}
|
|
key, err := SeatHoldersKey(name, node)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sh := out[key]
|
|
sh.V, sh.Seat, sh.Scope, sh.Node, sh.Outside = Version, name, h.Scope, node, !known
|
|
sh.Holders = append(sh.Holders, SeatHolder{Module: h.Module, Node: n})
|
|
out[key] = sh
|
|
}
|
|
}
|
|
for k, sh := range out {
|
|
sort.Slice(sh.Holders, func(i, j int) bool {
|
|
return sh.Holders[i].Node+"/"+sh.Holders[i].Module < sh.Holders[j].Node+"/"+sh.Holders[j].Module
|
|
})
|
|
out[k] = sh
|
|
}
|
|
return out
|
|
}
|
|
|
|
// HoldersOf reads who holds seat (on node, for a node seat) from the state's keys as a reader watched them: the node's
|
|
// key first, then the seat's. None when nobody holds it.
|
|
func HoldersOf(keys map[string][]byte, seat, node string) ([]SeatHolder, error) {
|
|
for _, n := range []string{node, ""} {
|
|
key, err := SeatHoldersKey(seat, n)
|
|
if err != nil {
|
|
if n == "" {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
raw, ok := keys[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
var sh SeatHolders
|
|
if err := json.Unmarshal(raw, &sh); err != nil {
|
|
return nil, fmt.Errorf("delivery: the holders of %s are not readable: %w", key, err)
|
|
}
|
|
if sh.V != Version {
|
|
return nil, fmt.Errorf("delivery: the holders of %s are of version %d, and this reads %d", key, sh.V, Version)
|
|
}
|
|
return sh.Holders, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// The builder's log (clause 1; the lead and the operator, 2026-10-29): the builder's own module log (ADR 0297) `build-log`,
|
|
// kept 30 days (max-age-days), one entry under the key `<module>` when a build of a fingerprint starts and one when it
|
|
// ends, so the newest entry of a key is the module's newest start or end. The builder's tools read it through the
|
|
// runtime's log verbs; another module reads it by naming BuildLogRead in its manifest's `reads`: a bundle through the
|
|
// runtime (stdio.Log(BuildLogRead)), a process on its own credential (process.Conn.ReadLog), each by newest per key,
|
|
// from a time, or following new entries. The catalogue module asks the builder's tool BuildsSinceTool instead.
|
|
const (
|
|
BuilderLog = "build-log"
|
|
BuildLogRead = BuilderModule + "." + BuilderLog
|
|
BuildStartedEvent = "build.started"
|
|
BuildEndedEvent = "build.ended"
|
|
// BuilderLogDays is how long the log keeps its entries.
|
|
BuilderLogDays = 30
|
|
// BuildsSinceTool is the builder's tool answering every ended build since a time, in order (clause 9): its
|
|
// argument `since` (RFC 3339), its answer BuildsSince.
|
|
BuildsSinceTool = "builder_builds_since"
|
|
)
|
|
|
|
// BuildLogKey is the key of a module's entries in the builder's log: the module's name, refused when a log key could not
|
|
// hold it.
|
|
func BuildLogKey(module string) (string, error) {
|
|
if err := checkName("module", module); err != nil {
|
|
return "", err
|
|
}
|
|
return module, nil
|
|
}
|
|
|
|
// BuildLogEntry is one entry of the builder's log: the module, the fingerprint, the source revision, where the module is
|
|
// built from (its repository, its directory there, the commit and the ref), the build node and the time; at the end,
|
|
// the outcome, the manifest built (its digest, read from the indexer's state `manifests` under ManifestsKey), what it was
|
|
// built against (the bases' references, as the controller's built-before said them) and each artifact made
|
|
// (BuiltArtifact, the done key's: its name, kind, digest and reference). The repository, directory and commit are what
|
|
// the indexer's PathOf, the outside-built registrations and the assigner's running commits read; the manifest, against
|
|
// and made what the catalogue module registers a build from (the lead, 2026-10-29).
|
|
type BuildLogEntry struct {
|
|
V int `json:"v"`
|
|
Event string `json:"event"`
|
|
Module string `json:"module"`
|
|
FP string `json:"fp"`
|
|
Source string `json:"source"`
|
|
Repository string `json:"repository"`
|
|
Directory string `json:"directory"`
|
|
Commit string `json:"commit"`
|
|
Ref string `json:"ref,omitempty"`
|
|
Node string `json:"node"`
|
|
At time.Time `json:"at"`
|
|
Outcome string `json:"outcome,omitempty"`
|
|
Manifest string `json:"manifest,omitempty"`
|
|
Against []string `json:"against,omitempty"`
|
|
Made []BuiltArtifact `json:"made,omitempty"`
|
|
}
|
|
|
|
// BuildsSince is the answer of BuildsSinceTool: every ended build since a time, oldest first, and whether the log held
|
|
// more than one answer carries (then ask again from the last one's time).
|
|
type BuildsSince struct {
|
|
Since time.Time `json:"since"`
|
|
Builds []BuildLogEntry `json:"builds"`
|
|
More bool `json:"more,omitempty"`
|
|
}
|
|
|
|
// NewestPerModule is each module's newest entry of entries (one kind, read from the last per subject or whole), by
|
|
// module: the reader's helper for the five newest and the running commits.
|
|
func NewestPerModule(entries []BuildLogEntry) map[string]BuildLogEntry {
|
|
out := map[string]BuildLogEntry{}
|
|
for _, e := range entries {
|
|
if have, ok := out[e.Module]; !ok || e.At.After(have.At) {
|
|
out[e.Module] = e
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// EndedSince is, of entries, the ended builds at or after since, oldest first: the reader's helper for history and for
|
|
// BuildsSinceTool's answer.
|
|
func EndedSince(entries []BuildLogEntry, since time.Time) []BuildLogEntry {
|
|
var out []BuildLogEntry
|
|
for _, e := range entries {
|
|
if e.Event == BuildEndedEvent && !e.At.Before(since) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) })
|
|
return out
|
|
}
|
|
|
|
// EncodeBuildLogEntry is e as JSON at Version; it refuses an entry a reader could not place.
|
|
func EncodeBuildLogEntry(e BuildLogEntry) ([]byte, error) {
|
|
e.V = Version
|
|
switch {
|
|
case e.Event != BuildStartedEvent && e.Event != BuildEndedEvent:
|
|
return nil, fmt.Errorf("delivery: a build log entry is %s or %s, not %q", BuildStartedEvent, BuildEndedEvent, e.Event)
|
|
case e.Module == "" || e.FP == "" || e.Node == "" || e.At.IsZero() || e.Repository == "" || e.Commit == "":
|
|
return nil, fmt.Errorf("delivery: a build log entry names its module, fingerprint, repository, commit, node and time")
|
|
case e.Event == BuildEndedEvent && e.Outcome == "":
|
|
return nil, fmt.Errorf("delivery: a build's end names its outcome")
|
|
case e.Event == BuildStartedEvent && (e.Outcome != "" || len(e.Made) > 0 || e.Manifest != "" || len(e.Against) > 0):
|
|
return nil, fmt.Errorf("delivery: a build's start names no outcome, manifest, base or artifact")
|
|
}
|
|
return json.Marshal(e)
|
|
}
|
|
|
|
// DecodeBuildLogEntry reads one entry.
|
|
func DecodeBuildLogEntry(data []byte) (BuildLogEntry, error) {
|
|
var e BuildLogEntry
|
|
if err := json.Unmarshal(data, &e); err != nil {
|
|
return BuildLogEntry{}, fmt.Errorf("delivery: a build log entry is not readable: %w", err)
|
|
}
|
|
if e.V != Version {
|
|
return BuildLogEntry{}, fmt.Errorf("delivery: a build log entry of version %d, and this reads %d", e.V, Version)
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
// BuildDuration is a build's duration, its end less its start (clause 1): false unless both are the same module's
|
|
// build of the same fingerprint, the start first.
|
|
func BuildDuration(start, end BuildLogEntry) (time.Duration, bool) {
|
|
if start.Event != BuildStartedEvent || end.Event != BuildEndedEvent || start.Module != end.Module || start.FP != end.FP ||
|
|
end.At.Before(start.At) {
|
|
return 0, false
|
|
}
|
|
return end.At.Sub(start.At), true
|
|
}
|
|
|
|
// **Which modules exist and which machines the mesh has, from the bus** (novox/hq ADR 0571 clauses 4, 5; the lead for a6,
|
|
// 2026-10-29): the tool runner reads the modules from the indexer's state `modules` and the machines from the placement
|
|
// seat's keys, and no longer asks the controller's `modules` or `nodes`.
|
|
|
|
// ModuleNames is every module the indexer holds, sorted, from its state `modules` as a reader holds its keys (key to
|
|
// value; a deleted key absent). A key that is not a module's is refused, naming it.
|
|
func ModuleNames(keys map[string][]byte) ([]string, error) {
|
|
out := make([]string, 0, len(keys))
|
|
for k := range keys {
|
|
module, err := ParseModulesKey(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, module)
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|
|
|
|
// MachineNames is every machine the placement seat places modules on, sorted, from its state `placement` as a reader
|
|
// holds its keys: each `placement.<node>`. Keys of the state's other kinds (a proposal's) are passed over.
|
|
func MachineNames(keys map[string][]byte) []string {
|
|
out := []string{}
|
|
for k := range keys {
|
|
if node, err := ParsePlacementKey(k); err == nil {
|
|
out = append(out, node)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|