Files
mesh-controller/internal/inventory/chain_builds.go
T
jschoubben e0fb241263 Keep a build request as the build in state asked (issue 1091)
A request is the build's record under the id its outcome will carry, in state asked; the outcome takes its place and keeps the request beside it, so assign, the pending assignments and the planner read the builds bucket alone. The bucket removes no key, so a request past thirty days is not listed instead of deleted. The move folds build_request into the same records (one kind, builds, counted against both tables) and marks the requests never handed over to be asked again once: claimed before it is asked and settled after, so a crash loses the ask rather than making it twice. Also tests the chain buckets' options on a real bus, the assignments' mirror options included.
2026-10-18 02:53:39 +02:00

571 lines
17 KiB
Go

package inventory
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/novox/mesh-controller/internal/chain"
)
// Builds on the bus (novox/hq ADR 0408, issue 1090): once the kind `builds` is moved (chain.go) every build is a key of
// the builds bucket, under its id, and every read below is made from the bucket's records. The tables are read when the
// kind is not moved, by the methods these stand beside.
// toChainBuild is a build as the bucket keeps it.
func toChainBuild(b Build) chain.Build {
c := chain.Build{ID: b.ID, Repository: b.Repository, Ref: b.Ref, Module: b.Module, Commit: b.Commit, On: b.On,
Path: b.Path, Against: b.Against, Mirrored: b.Mirrored, SourceFingerprint: b.SourceFingerprint,
Failed: b.Failed, Asked: b.Asked.UTC(), At: b.At.UTC(), ReuseKey: b.ReuseKey, Published: b.Published}
if len(b.Manifest) > 0 {
c.Manifest = json.RawMessage(b.Manifest)
}
for _, r := range b.Read {
c.Read = append(c.Read, chain.ReadRepository{Repository: r.Repository, Ref: r.Ref, Paths: r.Paths, Own: r.Own})
}
for _, s := range b.Sources {
c.Sources = append(c.Sources, chain.Source{Repository: s.Repository, Ref: s.Ref, Paths: s.Paths})
}
for _, a := range b.Made {
c.Made = append(c.Made, chain.Artifact{Name: a.Name, Kind: a.Kind, Reference: a.Reference})
}
if c.Published == nil {
c.Published = publishedOf(b.Made)
}
return c
}
// fromChainBuild is a build as the mesh reads it.
func fromChainBuild(c chain.Build) Build {
b := Build{ID: c.ID, Repository: c.Repository, Ref: c.Ref, Module: c.Module, Commit: c.Commit, On: c.On, Path: c.Path,
Manifest: []byte(c.Manifest), Against: c.Against, Mirrored: c.Mirrored, SourceFingerprint: c.SourceFingerprint,
Failed: c.Failed, Asked: c.Asked, At: c.At, ReuseKey: c.ReuseKey, Published: c.Published}
if len(b.Manifest) == 0 {
b.Manifest = nil
}
for _, r := range c.Read {
b.Read = append(b.Read, ReadRepository{Repository: r.Repository, Ref: r.Ref, Paths: r.Paths, Own: r.Own})
}
for _, s := range c.Sources {
b.Sources = append(b.Sources, BuildSource{Repository: s.Repository, Ref: s.Ref, Paths: s.Paths})
}
for _, a := range c.Made {
b.Made = append(b.Made, Artifact{Name: a.Name, Kind: a.Kind, Reference: a.Reference})
}
return b
}
// publishedOf is what a build published, read from what it made: a package by name and version, and an artifact by the
// digest of the first thing it made that has one (`<reference>@sha256:…`). Nil for a build that published nothing.
func publishedOf(made []Artifact) *chain.Published {
var out chain.Published
for _, a := range made {
switch {
case a.Kind == "package":
if name, version, ok := strings.Cut(a.Reference, "@"); ok && name != "" && version != "" && out.Package == nil {
out.Package = &chain.Package{Name: name, Version: version}
}
case out.Artifact == "":
if _, digest, ok := strings.Cut(a.Reference, "@"); ok && strings.HasPrefix(digest, "sha256:") {
out.Artifact = digest
}
}
}
if out.Artifact == "" && out.Package == nil {
return nil
}
return &out
}
// chainBuildKey is a build's key. An id the bus takes no spelling of is refused by the write, loudly.
func chainBuildKey(id string) string { return id }
// chainRecordBuild keeps a build under its id. **Idempotent on the id**, as the table's insert was: a result arrives twice,
// and the first record stands. A request nobody has answered is the record in state asked (issue 1091); the outcome
// takes its place under the same key, and keeps the request it answers.
func (i *Inventory) chainRecordBuild(ctx context.Context, c *chain.Chain, b Build) error {
if b.At.IsZero() {
b.At = time.Now().UTC()
}
key := chainBuildKey(b.ID)
for try := 0; try < casTries; try++ {
next := toChainBuild(b)
cur, found, err := c.Builds.Get(ctx, key)
if err != nil {
return fmt.Errorf("build %s is not kept on the bus: %w", b.ID, err)
}
switch {
case !found:
_, err = c.Builds.Create(ctx, key, next)
if errors.Is(err, chain.ErrExists) {
continue
}
case cur.Value.State == chain.BuildAsked:
next.Request = cur.Value.Request
_, err = c.Builds.Update(ctx, key, next, cur.Revision)
if errors.Is(err, chain.ErrMoved) {
continue
}
default:
// An outcome stands: the first record is the record.
return i.RecordMirrored(ctx, b.ID, "", b.Mirrored)
}
if err != nil {
return fmt.Errorf("build %s is not kept on the bus: %w", b.ID, err)
}
return i.RecordMirrored(ctx, b.ID, "", b.Mirrored)
}
return fmt.Errorf("build %s is not kept on the bus: it kept moving under the write", b.ID)
}
// casTries is how often a write that lost a compare-and-set reads the key again and tries once more.
const casTries = 8
// chainBuilds is every build of the bucket, whole, in no order.
func chainBuilds(ctx context.Context, c *chain.Chain) ([]Build, error) {
all, err := c.Builds.All(ctx)
if err != nil {
return nil, fmt.Errorf("the builds cannot be read from the bus: %w", err)
}
out := make([]Build, 0, len(all))
for _, k := range all {
// A request nobody has answered is no build: it has no outcome to read.
if k.Value.State == chain.BuildAsked {
continue
}
out = append(out, fromChainBuild(k.Value))
}
return out, nil
}
// newestRequestOrder sorts builds as newestRequestFirst does: the newest request first, then the newest record.
func newestRequestOrder(builds []Build) {
sort.SliceStable(builds, func(a, b int) bool {
x, y := builds[a].AskedOrAt(), builds[b].AskedOrAt()
if !x.Equal(y) {
return x.After(y)
}
if !builds[a].At.Equal(builds[b].At) {
return builds[a].At.After(builds[b].At)
}
return builds[a].ID > builds[b].ID
})
}
// byModule sorts builds by module, then newest request first.
func byModule(builds []Build) {
newestRequestOrder(builds)
sort.SliceStable(builds, func(a, b int) bool { return builds[a].Module < builds[b].Module })
}
// worked is the builds that made something for a module: succeeded, and knew what they built.
func worked(all []Build) []Build {
var out []Build
for _, b := range all {
if b.Failed == "" && b.Module != "" {
out = append(out, b)
}
}
return out
}
func (i *Inventory) chainBuildList(ctx context.Context, c *chain.Chain, module string, limit int) ([]Build, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
var out []Build
for _, b := range all {
if module == "" || b.Module == module {
out = append(out, Build{ID: b.ID, Repository: b.Repository, Ref: b.Ref, Module: b.Module, Commit: b.Commit,
On: b.On, Failed: b.Failed, Made: b.Made, Asked: b.Asked, At: b.At})
}
}
sort.SliceStable(out, func(a, b int) bool {
if !out[a].At.Equal(out[b].At) {
return out[a].At.After(out[b].At)
}
return out[a].ID > out[b].ID
})
if len(out) > limit {
out = out[:limit]
}
return out, nil
}
func (i *Inventory) chainBuildByID(ctx context.Context, c *chain.Chain, id string) (Build, bool, error) {
k, ok, err := c.Builds.Get(ctx, chainBuildKey(id))
if err != nil || !ok || k.Value.State == chain.BuildAsked {
// A request nobody has answered has no outcome to read.
return Build{}, false, err
}
b := fromChainBuild(k.Value)
return Build{ID: b.ID, Repository: b.Repository, Ref: b.Ref, Module: b.Module, Commit: b.Commit, On: b.On,
Failed: b.Failed, Made: b.Made, Path: b.Path, Asked: b.Asked, At: b.At}, true, nil
}
func (i *Inventory) chainBuiltAgainst(ctx context.Context, c *chain.Chain) (map[string][]string, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
ok := worked(all)
byModule(ok)
out := map[string][]string{}
seen := map[string]bool{}
for _, b := range ok {
if seen[b.Module] {
continue
}
seen[b.Module] = true
if len(b.Against) > 0 {
out[b.Module] = b.Against
}
}
return out, nil
}
func (i *Inventory) chainReadRepositories(ctx context.Context, c *chain.Chain) (map[string][]ReadRepository, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
// The newest build of each module whatever its outcome: a failed one newer than the newest that worked leaves that
// one's build source stale.
type newestOf struct {
at time.Time
failed bool
}
newest := map[string]newestOf{}
tried := make([]Build, 0, len(all))
for _, b := range all {
if b.Module != "" {
tried = append(tried, b)
}
}
byModule(tried)
for _, b := range tried {
if _, seen := newest[b.Module]; !seen {
newest[b.Module] = newestOf{b.AskedOrAt(), b.Failed != ""}
}
}
owns, err := i.moduleSources(ctx)
if err != nil {
return nil, err
}
ok := worked(all)
byModule(ok)
read := map[string][]ReadRepository{}
seen := map[string]bool{}
for _, b := range ok {
if seen[b.Module] {
continue
}
seen[b.Module] = true
built := b.AskedOrAt()
of := append([]ReadRepository(nil), b.Read...)
sources := b.Sources
whole := ""
if n, known := newest[b.Module]; known && n.failed && n.at.After(built) && len(sources) > 0 {
sources, whole = nil, "a newer build of it failed"
}
of = WithBuildSources(of, sources)
if own, has := owns[b.Module]; has {
of = OwnRepositoryAsOwn(of, own)
}
if whole != "" {
of = append(of, ReadRepository{Own: true, Whole: whole})
}
if len(of) > 0 {
for k := range of {
of[k].Built, of[k].Looked = built, newest[b.Module].at
}
read[b.Module] = of
}
}
return read, nil
}
// moduleSources is the repository each registered module is registered from.
func (i *Inventory) moduleSources(ctx context.Context) (map[string]string, error) {
rows, err := i.store.Pool().Query(ctx, `select name, source from module where source is not null`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]string{}
for rows.Next() {
var name, source string
if err := rows.Scan(&name, &source); err != nil {
return nil, err
}
out[name] = source
}
return out, rows.Err()
}
func (i *Inventory) chainAnnounceable(ctx context.Context, c *chain.Chain) ([]Build, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
var ok []Build
for _, b := range worked(all) {
if b.Commit != "" {
ok = append(ok, b)
}
}
newestRequestOrder(ok)
type key struct{ module, commit string }
seen := map[key]bool{}
var out []Build
for _, b := range ok {
k := key{b.Module, b.Commit}
if seen[k] {
continue
}
seen[k] = true
out = append(out, Build{ID: b.ID, Repository: b.Repository, Ref: b.Ref, Module: b.Module, Commit: b.Commit,
On: b.On, Failed: b.Failed, Made: b.Made, Path: b.Path, Manifest: b.Manifest, Against: b.Against, At: b.At})
}
sort.SliceStable(out, func(a, b int) bool { return out[a].At.Before(out[b].At) })
return out, nil
}
// failedAtAGate is the builds that failed their gate. The gate's verdicts stay in their table (issue 1097).
func (i *Inventory) failedAtAGate(ctx context.Context) (map[string]bool, error) {
rows, err := i.store.Pool().Query(ctx, `select build from build_gate where verdict = 'failed'`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]bool{}
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
out[id] = true
}
return out, rows.Err()
}
func (i *Inventory) chainSourceRows(ctx context.Context, c *chain.Chain, module string) (map[string][]sourceRow, []string, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, nil, err
}
failed, err := i.failedAtAGate(ctx)
if err != nil {
return nil, nil, err
}
ok := worked(all)
byModule(ok)
out := map[string][]sourceRow{}
var order []string
for _, b := range ok {
if module != "" && b.Module != module {
continue
}
made, err := json.Marshal(b.Made)
if err != nil {
return nil, nil, err
}
if _, seen := out[b.Module]; !seen {
order = append(order, b.Module)
}
out[b.Module] = append(out[b.Module], sourceRow{id: b.ID, commit: b.Commit, fingerprint: b.SourceFingerprint,
made: made, manifest: b.Manifest, failedGate: failed[b.ID]})
}
return out, order, nil
}
// canonical is a JSON document encoded the one way (objects' keys sorted, compact), so the same content hashes alike
// however the bucket's record spells it.
func canonicalJSON(raw []byte) []byte {
if len(bytes.TrimSpace(raw)) == 0 {
return nil
}
out, err := canonical(raw)
if err != nil {
return raw
}
return out
}
// chainFingerprint is what a build would put on a machine, hashed: its artifacts and its manifest.
func chainFingerprint(b Build) string {
made, _ := json.Marshal(b.Made)
sum := sha256.Sum256(append(append(canonicalJSON(made), 0), canonicalJSON(b.Manifest)...))
return hex.EncodeToString(sum[:])
}
func (i *Inventory) chainBuildFingerprints(ctx context.Context, c *chain.Chain, module string) (map[string]string, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
var ok []Build
for _, b := range all {
if b.Module == module && b.Failed == "" && b.Commit != "" {
ok = append(ok, b)
}
}
sortByRecorded(ok)
if len(ok) > 50 {
ok = ok[:50]
}
out := map[string]string{}
for _, b := range ok {
if _, seen := out[b.Commit]; !seen {
out[b.Commit] = chainFingerprint(b)
}
}
return out, nil
}
func (i *Inventory) chainFingerprints(ctx context.Context, c *chain.Chain) (map[string]map[string]string, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
var ok []Build
for _, b := range all {
if b.Module != "" && b.Failed == "" && b.Commit != "" {
ok = append(ok, b)
}
}
sortByRecorded(ok)
out := map[string]map[string]string{}
for _, b := range ok {
if out[b.Module] == nil {
out[b.Module] = map[string]string{}
}
if _, seen := out[b.Module][b.Commit]; !seen {
out[b.Module][b.Commit] = chainFingerprint(b)
}
}
return out, nil
}
// sortByRecorded is newest record first, as `order by at desc`.
func sortByRecorded(builds []Build) {
sort.SliceStable(builds, func(a, b int) bool {
if !builds[a].At.Equal(builds[b].At) {
return builds[a].At.After(builds[b].At)
}
return builds[a].ID > builds[b].ID
})
}
func (i *Inventory) chainBuildOf(ctx context.Context, c *chain.Chain, module, commit string) (string, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return "", err
}
var ok []Build
for _, b := range all {
if b.Module == module && b.Commit == commit && b.Failed == "" {
ok = append(ok, b)
}
}
sortByRecorded(ok)
if len(ok) == 0 {
return "", nil
}
return ok[0].ID, nil
}
// chainManifestBuild is the newest successful build of a module from a commit (or one it abbreviates) that kept a manifest.
func (i *Inventory) chainManifestBuild(ctx context.Context, c *chain.Chain, module, commit string) (string, []byte, bool, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return "", nil, false, err
}
var ok []Build
for _, b := range all {
if b.Module == module && b.Failed == "" && len(b.Manifest) > 0 && string(bytes.TrimSpace(b.Manifest)) != "null" &&
(b.Commit == commit || strings.HasPrefix(b.Commit, commit)) {
ok = append(ok, b)
}
}
sortByRecorded(ok)
if len(ok) == 0 {
return "", nil, false, nil
}
return ok[0].ID, ok[0].Manifest, true, nil
}
// chainManifestOf is the manifest a build was recorded with.
func (i *Inventory) chainManifestOf(ctx context.Context, c *chain.Chain, id string) ([]byte, error) {
k, ok, err := c.Builds.Get(ctx, chainBuildKey(id))
if err != nil || !ok {
return nil, err
}
return []byte(k.Value.Manifest), nil
}
// chainRecentMade is the artifacts and bases of the KeptBuilds newest successful builds of each registered module.
func (i *Inventory) chainRecentMade(ctx context.Context, c *chain.Chain) ([]Build, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
rows, err := i.store.Pool().Query(ctx, `select name from module`)
if err != nil {
return nil, err
}
registered := map[string]bool{}
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
rows.Close()
return nil, err
}
registered[name] = true
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
var ok []Build
for _, b := range worked(all) {
if registered[b.Module] {
ok = append(ok, b)
}
}
sortByRecorded(ok)
back := map[string]int{}
var out []Build
for _, b := range ok {
back[b.Module]++
if back[b.Module] <= KeptBuilds {
out = append(out, b)
}
}
return out, nil
}
// chainEveryReferenceMade is every successful build's artifacts, oldest build first.
func (i *Inventory) chainEveryReferenceMade(ctx context.Context, c *chain.Chain) ([]Build, error) {
all, err := chainBuilds(ctx, c)
if err != nil {
return nil, err
}
ok := worked(all)
sort.SliceStable(ok, func(a, b int) bool {
if !ok[a].At.Equal(ok[b].At) {
return ok[a].At.Before(ok[b].At)
}
return ok[a].ID < ok[b].ID
})
return ok, nil
}