Files
mesh-sdk/go/delivery/history_records.go
T

185 lines
6.6 KiB
Go

package delivery
import (
"encoding/json"
"fmt"
"sort"
"time"
)
// **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.
type SeatHolders struct {
V int `json:"v"`
Seat string `json:"seat"`
Scope string `json:"scope"`
Node string `json:"node,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. 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
}
key, err := SeatHoldersKey(h.Claim, node)
if err != nil {
continue
}
sh := out[key]
sh.V, sh.Seat, sh.Scope, sh.Node = Version, h.Claim, h.Scope, node
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): a stream of its own, `log`, one entry when a build of a fingerprint starts and one when
// it ends, kept 30 days.
const (
BuilderLogStream = "log"
BuildStartedEvent = "build.started"
BuildEndedEvent = "build.ended"
// BuilderLogDays is how long the log keeps its entries.
BuilderLogDays = 30
)
// BuiltDigest is one artifact a build ended with, by its name and digest.
type BuiltDigest struct {
Name string `json:"name"`
Digest string `json:"digest"`
}
// 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 and the commit), the build node and the time; at the end, the outcome
// and each artifact's digest. The repository, directory and commit are what the indexer's PathOf, the outside-built
// registrations and the assigner's running commits read (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"`
Node string `json:"node"`
At time.Time `json:"at"`
Outcome string `json:"outcome,omitempty"`
Artifacts []BuiltDigest `json:"artifacts,omitempty"`
}
// 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.Artifacts) > 0):
return nil, fmt.Errorf("delivery: a build's start names no outcome and no 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
}