Delivery 2 deleted the move and chain_moved, so the layout of the builds under their modules is made by the controller's start (LayBuildsOut), recorded by the key layout.done in the builds bucket, retried every half minute with an urgent condition while it fails; the chain on the bus is refused until it exists. Its source readers use the module and pointer reads.
1280 lines
42 KiB
Go
1280 lines
42 KiB
Go
package inventory
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"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,
|
|
ManifestDigest: b.ManifestDigest, 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), ManifestDigest: c.ManifestDigest, 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
|
|
}
|
|
|
|
// legacyBuildKey is the key a build had before the layout under its module (issue 1121): its id alone. The keys stay (ADR
|
|
// 0408 §4) and nothing reads them once the kind builds-layout is moved.
|
|
func legacyBuildKey(id string) string { return id }
|
|
|
|
// chainRecordBuild keeps a build under its module and 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 `req.<id>` in state asked (issue
|
|
// 1091); the outcome is written under its module and keeps the request it answers, and the request's record then says it was
|
|
// answered. The build's manifest goes to the manifests bucket once, and the record names its digest (issue 1121).
|
|
func (i *Inventory) chainRecordBuild(ctx context.Context, c *chain.Chain, b Build) error {
|
|
if b.At.IsZero() {
|
|
b.At = time.Now().UTC()
|
|
}
|
|
if slices.Contains(chain.ReservedModules, b.Module) {
|
|
return fmt.Errorf("build %s is not kept on the bus: %q is a name the bucket keeps for itself, not a module's", b.ID, b.Module)
|
|
}
|
|
digest, err := i.keepManifest(ctx, c, b.Manifest)
|
|
if err != nil {
|
|
return fmt.Errorf("build %s is not kept on the bus: its manifest: %w", b.ID, err)
|
|
}
|
|
key, reqKey := chain.BuildKey(b.Module, b.ID), chain.RequestKey(b.ID)
|
|
for try := 0; try < casTries; try++ {
|
|
next := toChainBuild(b)
|
|
if digest != "" {
|
|
next.Manifest, next.ManifestDigest = nil, digest
|
|
}
|
|
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)
|
|
}
|
|
var asked chain.Kept[chain.Build]
|
|
var askedFound bool
|
|
if key != reqKey {
|
|
if asked, askedFound, err = c.Builds.Get(ctx, reqKey); err != nil {
|
|
return fmt.Errorf("build %s is not kept on the bus: %w", b.ID, err)
|
|
}
|
|
}
|
|
switch {
|
|
case !found && askedFound:
|
|
next.Request = asked.Value.Request
|
|
_, err = c.Builds.Create(ctx, key, next)
|
|
if errors.Is(err, chain.ErrExists) {
|
|
continue
|
|
}
|
|
case !found:
|
|
_, err = c.Builds.Create(ctx, key, next)
|
|
if errors.Is(err, chain.ErrExists) {
|
|
continue
|
|
}
|
|
case key == reqKey && 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. Its id pointer is made if a crash left it out.
|
|
if _, err := i.pointTo(ctx, c, chain.IDKey(b.ID), idPointer(b.Module), false); err != nil {
|
|
return err
|
|
}
|
|
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)
|
|
}
|
|
if err := i.pointAt(ctx, c, next, chainFingerprint(fromChainBuild(next))); err != nil {
|
|
return err
|
|
}
|
|
if askedFound && asked.Value.State == chain.BuildAsked {
|
|
i.answerRequest(ctx, c, reqKey, next)
|
|
}
|
|
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)
|
|
}
|
|
|
|
// idPointer is what `id.<id>` holds: the build's module, or the request prefix for a build that knew none.
|
|
func idPointer(module string) []byte {
|
|
if module == "" {
|
|
return []byte(chain.RequestPrefix)
|
|
}
|
|
return []byte(module)
|
|
}
|
|
|
|
// pointTo makes a pointer key hold value, by compare-and-set: made when it is absent, and, when overwrite, changed when it
|
|
// holds something else.
|
|
func (i *Inventory) pointTo(ctx context.Context, c *chain.Chain, key string, value []byte, overwrite bool) (bool, error) {
|
|
for try := 0; try < casTries; try++ {
|
|
e, found, err := c.Builds.GetRaw(ctx, key)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s is not read on the bus: %w", key, err)
|
|
}
|
|
switch {
|
|
case !found:
|
|
_, err = c.Builds.CreateRaw(ctx, key, value)
|
|
if errors.Is(err, chain.ErrExists) {
|
|
continue
|
|
}
|
|
case !overwrite || bytes.Equal(e.Value, value):
|
|
return false, nil
|
|
default:
|
|
_, err = c.Builds.UpdateRaw(ctx, key, value, e.Revision)
|
|
if errors.Is(err, chain.ErrMoved) {
|
|
continue
|
|
}
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s is not kept on the bus: %w", key, err)
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("%s kept moving under the write", key)
|
|
}
|
|
|
|
// pointAt moves the pointers a build written now is the answer to: its id's module, the module's newest, and for a success
|
|
// the module's worked, held, commit and source (issue 1121).
|
|
func (i *Inventory) pointAt(ctx context.Context, c *chain.Chain, rec chain.Build, fingerprint string) error {
|
|
if _, err := i.pointTo(ctx, c, chain.IDKey(rec.ID), idPointer(rec.Module), false); err != nil {
|
|
return err
|
|
}
|
|
if rec.Module == "" {
|
|
return nil
|
|
}
|
|
if _, err := i.pointIfNewer(ctx, c, chain.NewestKey(rec.Module), []byte(rec.ID), rec); err != nil {
|
|
return err
|
|
}
|
|
if rec.Failed != "" {
|
|
return nil
|
|
}
|
|
// Held: the run of builds made from one source stands on its first. The previous success is read before it is replaced;
|
|
// a build older than it, finishing late, moves neither.
|
|
hold, older := true, false
|
|
if prev, found, err := c.Builds.GetRaw(ctx, chain.WorkedKey(rec.Module)); err != nil {
|
|
return err
|
|
} else if found {
|
|
if old, found, err := c.Builds.Get(ctx, chain.BuildKey(rec.Module, pointedID(prev.Value))); err != nil {
|
|
return err
|
|
} else if found {
|
|
older = !buildNewer(rec, old.Value)
|
|
hold = rec.SourceFingerprint == "" || old.Value.SourceFingerprint != rec.SourceFingerprint
|
|
}
|
|
}
|
|
if !older {
|
|
if _, err := i.pointIfNewer(ctx, c, chain.WorkedKey(rec.Module), []byte(rec.ID), rec); err != nil {
|
|
return err
|
|
}
|
|
if _, found, err := c.Builds.GetRaw(ctx, chain.HeldKey(rec.Module)); err != nil {
|
|
return err
|
|
} else if hold || !found {
|
|
if _, err := i.pointIfNewer(ctx, c, chain.HeldKey(rec.Module), []byte(rec.ID), rec); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if rec.Commit != "" {
|
|
body, err := json.Marshal(chain.CommitPointer{Build: rec.ID, Source: rec.SourceFingerprint, Fingerprint: fingerprint})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := i.pointIfNewer(ctx, c, chain.CommitKey(rec.Module, rec.Commit), body, rec); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if rec.ReuseKey != "" {
|
|
_, err := i.pointIfNewer(ctx, c, chain.SourceKey(rec.Module, chain.SpellReuse(rec.ReuseKey)), []byte(rec.ID), rec)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// buildNewer is whether a is a newer build than b in the order every "what a module currently is" question uses: the newest
|
|
// request first, then the newest record, then the id (newestRequestFirst).
|
|
func buildNewer(a, b chain.Build) bool {
|
|
x, y := fromChainBuild(a).AskedOrAt(), fromChainBuild(b).AskedOrAt()
|
|
if !x.Equal(y) {
|
|
return x.After(y)
|
|
}
|
|
if !a.At.Equal(b.At) {
|
|
return a.At.After(b.At)
|
|
}
|
|
return a.ID > b.ID
|
|
}
|
|
|
|
// pointIfNewer makes a pointer name rec by compare-and-set on the pointer's own revision, unless the build it names now is
|
|
// newer: **a pointer follows the newest build, not the last write** (issue 1121), so an older build that finishes late
|
|
// leaves it alone.
|
|
func (i *Inventory) pointIfNewer(ctx context.Context, c *chain.Chain, key string, value []byte, rec chain.Build) (bool, error) {
|
|
for try := 0; try < casTries; try++ {
|
|
e, found, err := c.Builds.GetRaw(ctx, key)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s is not read on the bus: %w", key, err)
|
|
}
|
|
if found {
|
|
if bytes.Equal(e.Value, value) {
|
|
return false, nil
|
|
}
|
|
cur, has, err := c.Builds.Get(ctx, chain.BuildKey(rec.Module, pointedID(e.Value)))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if has && cur.Value.State == "" && !buildNewer(rec, cur.Value) {
|
|
return false, nil
|
|
}
|
|
_, err = c.Builds.UpdateRaw(ctx, key, value, e.Revision)
|
|
if errors.Is(err, chain.ErrMoved) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s is not kept on the bus: %w", key, err)
|
|
}
|
|
return true, nil
|
|
}
|
|
_, err = c.Builds.CreateRaw(ctx, key, value)
|
|
if errors.Is(err, chain.ErrExists) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s is not kept on the bus: %w", key, err)
|
|
}
|
|
return true, nil
|
|
}
|
|
return false, fmt.Errorf("%s kept moving under the write", key)
|
|
}
|
|
|
|
// answerRequest says on the request's record that a build answered it. Said and not failed on: the outcome stands, and the
|
|
// request is read as unheard until a later write makes this one.
|
|
func (i *Inventory) answerRequest(ctx context.Context, c *chain.Chain, reqKey string, out chain.Build) {
|
|
for try := 0; try < casTries; try++ {
|
|
cur, found, err := c.Builds.Get(ctx, reqKey)
|
|
if err != nil || !found || cur.Value.State != chain.BuildAsked {
|
|
return
|
|
}
|
|
cur.Value.State, cur.Value.Module, cur.Value.Failed, cur.Value.At = chain.BuildAnswered, out.Module, out.Failed, out.At
|
|
if _, err := c.Builds.Update(ctx, reqKey, cur.Value, cur.Revision); !errors.Is(err, chain.ErrMoved) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// manifestBytes is a manifest as the bucket keeps it: its canonical form, nil when the build kept none.
|
|
func manifestBytes(raw []byte) []byte {
|
|
if t := bytes.TrimSpace(raw); len(t) == 0 || string(t) == "null" {
|
|
return nil
|
|
}
|
|
return canonicalJSON(raw)
|
|
}
|
|
|
|
// keepManifest keeps a manifest in the manifests bucket under the digest of its canonical bytes, once, and answers the
|
|
// digest; empty when the build kept none or the chain keeps manifests in the record.
|
|
func (i *Inventory) keepManifest(ctx context.Context, c *chain.Chain, raw []byte) (string, error) {
|
|
body := manifestBytes(raw)
|
|
if body == nil || !c.Kept {
|
|
return "", nil
|
|
}
|
|
key := chain.ManifestKey(body)
|
|
if _, err := c.Manifests.CreateRaw(ctx, key, body); err != nil && !errors.Is(err, chain.ErrExists) {
|
|
return "", err
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
// manifestOfRecord is the manifest a record names, or keeps.
|
|
func manifestOfRecord(ctx context.Context, c *chain.Chain, rec chain.Build) ([]byte, error) {
|
|
if rec.ManifestDigest == "" {
|
|
return []byte(rec.Manifest), nil
|
|
}
|
|
e, found, err := c.Manifests.GetRaw(ctx, rec.ManifestDigest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the manifest %s of build %s cannot be read: %w", rec.ManifestDigest, rec.ID, err)
|
|
}
|
|
if !found {
|
|
return nil, fmt.Errorf("build %s names the manifest %s and the bus does not hold it", rec.ID, rec.ManifestDigest)
|
|
}
|
|
return e.Value, nil
|
|
}
|
|
|
|
// casTries is how often a write that lost a compare-and-set reads the key again and tries once more.
|
|
const casTries = 8
|
|
|
|
// buildsIn is the records of the builds bucket under the keys a filter matches, read in one pass of a watch the server
|
|
// filters; the pointers among them are left out.
|
|
func buildsIn(ctx context.Context, c *chain.Chain, filter string) ([]chain.Kept[chain.Build], error) {
|
|
raw, err := c.Builds.FilteredRaw(ctx, filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the builds cannot be read from the bus: %w", err)
|
|
}
|
|
out := make([]chain.Kept[chain.Build], 0, len(raw))
|
|
for key, e := range raw {
|
|
if !chain.IsRecordKey(key) && !strings.HasPrefix(key, chain.RequestPrefix+".") {
|
|
continue
|
|
}
|
|
k, err := chain.DecodeKept[chain.Build](e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, k)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func outcomesOf(kept []chain.Kept[chain.Build]) []Build {
|
|
out := make([]Build, 0, len(kept))
|
|
for _, k := range kept {
|
|
// A request nobody has answered is no build: it has no outcome to read.
|
|
if k.Value.State == chain.BuildAsked || k.Value.State == chain.BuildAnswered {
|
|
continue
|
|
}
|
|
out = append(out, fromChainBuild(k.Value))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// chainBuilds is every build of every module, whole, in no order: the modules named by their `newest` pointers, each read by
|
|
// its own keys (the manifests kept in the same bucket are never in a filter), and the builds that knew no module.
|
|
func chainBuilds(ctx context.Context, c *chain.Chain) ([]Build, error) {
|
|
pointers, err := c.Builds.FilteredRaw(ctx, "*.newest")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the builds cannot be read from the bus: %w", err)
|
|
}
|
|
var out []Build
|
|
for key := range pointers {
|
|
module := chain.ModuleOf(key)
|
|
if slices.Contains(chain.ReservedModules, module) {
|
|
continue
|
|
}
|
|
builds, err := chainModuleBuilds(ctx, c, module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, builds...)
|
|
}
|
|
kept, err := buildsIn(ctx, c, chain.RequestPrefix+".*")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(out, outcomesOf(kept)...), nil
|
|
}
|
|
|
|
// chainModuleBuilds is every build of one module, in no order: the keys `<module>.>` and no other.
|
|
func chainModuleBuilds(ctx context.Context, c *chain.Chain, module string) ([]Build, error) {
|
|
kept, err := buildsIn(ctx, c, chain.ModuleFilter(module))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return outcomesOf(kept), nil
|
|
}
|
|
|
|
// chainBuildsOf is the builds of a module, or of every module when none is named.
|
|
func chainBuildsOf(ctx context.Context, c *chain.Chain, module string) ([]Build, error) {
|
|
if module == "" {
|
|
return chainBuilds(ctx, c)
|
|
}
|
|
return chainModuleBuilds(ctx, c, module)
|
|
}
|
|
|
|
// pointedBuilds is, per module, the build a family of pointers (`*.newest`, `*.worked`, `*.held`) names, each read by one
|
|
// Get and checked: a survey of every module reads one record of each and not their histories.
|
|
func (i *Inventory) pointedBuilds(ctx context.Context, c *chain.Chain, filter string) (map[string]chain.Build, error) {
|
|
raw, err := c.Builds.FilteredRaw(ctx, filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the builds cannot be read from the bus: %w", err)
|
|
}
|
|
worked := strings.HasSuffix(filter, ".worked")
|
|
out := make(map[string]chain.Build, len(raw))
|
|
for key := range raw {
|
|
module := chain.ModuleOf(key)
|
|
if slices.Contains(chain.ReservedModules, module) {
|
|
continue
|
|
}
|
|
rec, found, err := i.followPointer(ctx, c, module, key, func(b chain.Build) bool { return !worked || b.Failed == "" })
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if found {
|
|
out[module] = rec
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// pointedID is the build id a pointer holds: a commit pointer's build, or the id itself.
|
|
func pointedID(value []byte) string {
|
|
if len(value) > 0 && value[0] == '{' {
|
|
var p chain.CommitPointer
|
|
if json.Unmarshal(value, &p) == nil {
|
|
return p.Build
|
|
}
|
|
}
|
|
return string(value)
|
|
}
|
|
|
|
// followPointer reads the record a pointer of a module names, and checks it: it is the module's, it is an outcome, and ok
|
|
// agrees. **A pointer is a hint** (issue 1121): when it is missing or the record is not what it was for, the module's records
|
|
// are read, the pointers made again and said, and the pointer is followed once more.
|
|
func (i *Inventory) followPointer(ctx context.Context, c *chain.Chain, module, pointer string, ok func(chain.Build) bool) (chain.Build, bool, error) {
|
|
for pass := 0; pass < 2; pass++ {
|
|
e, found, err := c.Builds.GetRaw(ctx, pointer)
|
|
if err != nil {
|
|
return chain.Build{}, false, err
|
|
}
|
|
if found {
|
|
k, has, err := c.Builds.Get(ctx, chain.BuildKey(module, pointedID(e.Value)))
|
|
if err != nil {
|
|
return chain.Build{}, false, err
|
|
}
|
|
if has && k.Value.Module == module && k.Value.State == "" && ok(k.Value) {
|
|
return k.Value, true, nil
|
|
}
|
|
}
|
|
if pass == 1 {
|
|
break
|
|
}
|
|
if err := i.repairPointers(ctx, c, module, pointer); err != nil {
|
|
return chain.Build{}, false, err
|
|
}
|
|
}
|
|
return chain.Build{}, false, nil
|
|
}
|
|
|
|
// repairPointers reads a module's records and makes its pointers what they say, saying each it changed.
|
|
func (i *Inventory) repairPointers(ctx context.Context, c *chain.Chain, module, why string) error {
|
|
kept, err := buildsIn(ctx, c, chain.ModuleFilter(module))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var recs []chain.Build
|
|
for _, k := range kept {
|
|
if k.Value.State == "" {
|
|
recs = append(recs, k.Value)
|
|
}
|
|
}
|
|
points, err := pointsOf(module, recs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, p := range points {
|
|
wrote, err := i.pointTo(ctx, c, p.key, p.value, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if wrote {
|
|
fmt.Printf("builds: the pointer %s was missing or wrong (a lookup of %s) and is repaired from the records of %s\n", p.key, why, module)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type pointer struct {
|
|
key string
|
|
value []byte
|
|
}
|
|
|
|
// pointsOf are the pointers of a module as its records say: the newest, the newest success, the held, and per commit and per
|
|
// reuse key the newest success. The records are in no order.
|
|
func pointsOf(module string, recs []chain.Build) ([]pointer, error) {
|
|
if len(recs) == 0 {
|
|
return nil, nil
|
|
}
|
|
sort.SliceStable(recs, func(a, b int) bool { return buildNewer(recs[b], recs[a]) })
|
|
points := []pointer{{chain.NewestKey(module), []byte(recs[len(recs)-1].ID)}}
|
|
var lastWorked, held *chain.Build
|
|
commits, sources := map[string]chain.Build{}, map[string]chain.Build{}
|
|
for k := range recs {
|
|
rec := recs[k]
|
|
if rec.Failed != "" {
|
|
continue
|
|
}
|
|
if held == nil || rec.SourceFingerprint == "" || lastWorked == nil || lastWorked.SourceFingerprint != rec.SourceFingerprint {
|
|
held = &recs[k]
|
|
}
|
|
lastWorked = &recs[k]
|
|
if rec.Commit != "" {
|
|
commits[rec.Commit] = rec
|
|
}
|
|
if rec.ReuseKey != "" {
|
|
sources[rec.ReuseKey] = rec
|
|
}
|
|
}
|
|
if lastWorked != nil {
|
|
points = append(points, pointer{chain.WorkedKey(module), []byte(lastWorked.ID)}, pointer{chain.HeldKey(module), []byte(held.ID)})
|
|
}
|
|
for commit, rec := range commits {
|
|
body, err := json.Marshal(chain.CommitPointer{Build: rec.ID, Source: rec.SourceFingerprint, Fingerprint: chainFingerprint(fromChainBuild(rec))})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
points = append(points, pointer{chain.CommitKey(module, commit), body})
|
|
}
|
|
for key, rec := range sources {
|
|
points = append(points, pointer{chain.SourceKey(module, chain.SpellReuse(key)), []byte(rec.ID)})
|
|
}
|
|
return points, 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 := chainBuildsOf(ctx, c, module)
|
|
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, ReuseKey: b.ReuseKey, Published: b.Published})
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
// chainRecordOf is the record of a build known by its id alone: the pointer `id.<id>` names its module, and the record is
|
|
// one Get more. A request no outcome has answered is no build.
|
|
func (i *Inventory) chainRecordOf(ctx context.Context, c *chain.Chain, id string) (chain.Build, bool, error) {
|
|
ptr, found, err := c.Builds.GetRaw(ctx, chain.IDKey(id))
|
|
if err != nil {
|
|
return chain.Build{}, false, err
|
|
}
|
|
module := ""
|
|
if found && string(ptr.Value) != chain.RequestPrefix {
|
|
module = string(ptr.Value)
|
|
}
|
|
k, ok, err := c.Builds.Get(ctx, chain.BuildKey(module, id))
|
|
if err != nil || !ok || k.Value.State == chain.BuildAsked || k.Value.State == chain.BuildAnswered {
|
|
return chain.Build{}, false, err
|
|
}
|
|
return k.Value, true, nil
|
|
}
|
|
|
|
func (i *Inventory) chainBuildByID(ctx context.Context, c *chain.Chain, id string) (Build, bool, error) {
|
|
rec, ok, err := i.chainRecordOf(ctx, c, id)
|
|
if err != nil || !ok {
|
|
return Build{}, false, err
|
|
}
|
|
b := fromChainBuild(rec)
|
|
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) {
|
|
worked, err := i.pointedBuilds(ctx, c, "*.worked")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string][]string{}
|
|
for module, b := range worked {
|
|
if len(b.Against) > 0 {
|
|
out[module] = b.Against
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (i *Inventory) chainReadRepositories(ctx context.Context, c *chain.Chain) (map[string][]ReadRepository, error) {
|
|
// 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. Each is one record read through its pointer.
|
|
type newestOf struct {
|
|
at time.Time
|
|
failed bool
|
|
}
|
|
tried, err := i.pointedBuilds(ctx, c, "*.newest")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
newest := map[string]newestOf{}
|
|
for module, rec := range tried {
|
|
b := fromChainBuild(rec)
|
|
newest[module] = newestOf{b.AskedOrAt(), b.Failed != ""}
|
|
}
|
|
workedRecs, err := i.pointedBuilds(ctx, c, "*.worked")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ok := make([]Build, 0, len(workedRecs))
|
|
for _, rec := range workedRecs {
|
|
ok = append(ok, fromChainBuild(rec))
|
|
}
|
|
byModule(ok)
|
|
owns, err := i.moduleSources(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
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, ManifestDigest: b.ManifestDigest,
|
|
Against: b.Against, At: b.At})
|
|
}
|
|
sort.SliceStable(out, func(a, b int) bool { return out[a].At.Before(out[b].At) })
|
|
if err := fillManifests(ctx, c, out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// fillManifests reads the manifests the builds name, each digest once.
|
|
func fillManifests(ctx context.Context, c *chain.Chain, builds []Build) error {
|
|
seen := map[string][]byte{}
|
|
for k := range builds {
|
|
d := builds[k].ManifestDigest
|
|
if d == "" || len(builds[k].Manifest) > 0 {
|
|
continue
|
|
}
|
|
raw, ok := seen[d]
|
|
if !ok {
|
|
var err error
|
|
if raw, err = manifestOfRecord(ctx, c, chain.Build{ID: builds[k].ID, ManifestDigest: d}); err != nil {
|
|
return err
|
|
}
|
|
seen[d] = raw
|
|
}
|
|
builds[k].Manifest = raw
|
|
}
|
|
return 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 := chainBuildsOf(ctx, c, module)
|
|
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, manifestDigest: b.ManifestDigest, 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, the manifest by its
|
|
// digest (the digest of its canonical bytes, which the manifests bucket keys it by).
|
|
func chainFingerprint(b Build) string {
|
|
made, _ := json.Marshal(b.Made)
|
|
digest := b.ManifestDigest
|
|
if digest == "" {
|
|
if body := manifestBytes(b.Manifest); body != nil {
|
|
digest = chain.ManifestKey(body)
|
|
}
|
|
}
|
|
sum := sha256.Sum256(append(append(canonicalJSON(made), 0), digest...))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
// commitPointers are, per commit of a module (every module when none is named), the pointer to the newest successful build
|
|
// from it: a read of the keys `<module>.commit.*` and no record.
|
|
func commitPointers(ctx context.Context, c *chain.Chain, module string) (map[string]map[string]chain.CommitPointer, error) {
|
|
filter := "*.commit.*"
|
|
if module != "" {
|
|
filter = module + ".commit.*"
|
|
}
|
|
raw, err := c.Builds.FilteredRaw(ctx, filter)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the builds cannot be read from the bus: %w", err)
|
|
}
|
|
out := map[string]map[string]chain.CommitPointer{}
|
|
for key, e := range raw {
|
|
var p chain.CommitPointer
|
|
if err := json.Unmarshal(e.Value, &p); err != nil {
|
|
return nil, fmt.Errorf("the pointer %s is not readable: %w", key, err)
|
|
}
|
|
m, commit, _ := strings.Cut(key, ".commit.")
|
|
if out[m] == nil {
|
|
out[m] = map[string]chain.CommitPointer{}
|
|
}
|
|
out[m][commit] = p
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (i *Inventory) chainBuildFingerprints(ctx context.Context, c *chain.Chain, module string) (map[string]string, error) {
|
|
pointers, err := commitPointers(ctx, c, module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]string{}
|
|
for commit, p := range pointers[module] {
|
|
out[commit] = p.Fingerprint
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (i *Inventory) chainFingerprints(ctx context.Context, c *chain.Chain) (map[string]map[string]string, error) {
|
|
pointers, err := commitPointers(ctx, c, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]map[string]string{}
|
|
for m, commits := range pointers {
|
|
out[m] = map[string]string{}
|
|
for commit, p := range commits {
|
|
out[m][commit] = p.Fingerprint
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// chainBuildSourceFingerprints is chainSourceFingerprints for one module: commit → fingerprint, from its commit pointers.
|
|
func (i *Inventory) chainBuildSourceFingerprints(ctx context.Context, c *chain.Chain, module string) (map[string]string, error) {
|
|
pointers, err := commitPointers(ctx, c, module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]string{}
|
|
for commit, p := range pointers[module] {
|
|
if p.Source != "" {
|
|
out[commit] = p.Source
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// chainSourceFingerprints is, per module and commit, the source fingerprint of the newest successful build from it that has
|
|
// one, read from the commit pointers.
|
|
func (i *Inventory) chainSourceFingerprints(ctx context.Context, c *chain.Chain) (map[string]map[string]string, error) {
|
|
pointers, err := commitPointers(ctx, c, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]map[string]string{}
|
|
for m, commits := range pointers {
|
|
for commit, p := range commits {
|
|
if p.Source == "" {
|
|
continue
|
|
}
|
|
if out[m] == nil {
|
|
out[m] = map[string]string{}
|
|
}
|
|
out[m][commit] = p.Source
|
|
}
|
|
}
|
|
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
|
|
})
|
|
}
|
|
|
|
// chainBuildOf is the id of the newest successful build of a module from a commit: its commit pointer, and the record it names.
|
|
func (i *Inventory) chainBuildOf(ctx context.Context, c *chain.Chain, module, commit string) (string, error) {
|
|
rec, found, err := i.followPointer(ctx, c, module, chain.CommitKey(module, commit),
|
|
func(b chain.Build) bool { return b.Failed == "" && b.Commit == commit })
|
|
if err != nil || !found {
|
|
return "", err
|
|
}
|
|
return rec.ID, nil
|
|
}
|
|
|
|
// chainBuildOfSource is the id of the newest successful build of a module with a reuse key (ADR 0350 §1): its source
|
|
// pointer, and the record it names. What a reuse asks.
|
|
func (i *Inventory) chainBuildOfSource(ctx context.Context, c *chain.Chain, module, reuseKey string) (string, error) {
|
|
rec, found, err := i.followPointer(ctx, c, module, chain.SourceKey(module, chain.SpellReuse(reuseKey)),
|
|
func(b chain.Build) bool { return b.Failed == "" && b.ReuseKey == reuseKey })
|
|
if err != nil || !found {
|
|
return "", err
|
|
}
|
|
return rec.ID, nil
|
|
}
|
|
|
|
// chainManifestBuild is the newest successful build of a module from a commit (or one it abbreviates) that kept a manifest.
|
|
// A whole commit is its pointer and the record; an abbreviation reads the module's builds.
|
|
func (i *Inventory) chainManifestBuild(ctx context.Context, c *chain.Chain, module, commit string) (string, []byte, bool, error) {
|
|
kept := func(b chain.Build) bool { return b.ManifestDigest != "" || manifestBytes(b.Manifest) != nil }
|
|
if rec, found, err := i.followPointer(ctx, c, module, chain.CommitKey(module, commit),
|
|
func(b chain.Build) bool { return b.Failed == "" && b.Commit == commit }); err != nil {
|
|
return "", nil, false, err
|
|
} else if found && kept(rec) {
|
|
raw, err := manifestOfRecord(ctx, c, rec)
|
|
return rec.ID, raw, err == nil, err
|
|
}
|
|
all, err := chainModuleBuilds(ctx, c, module)
|
|
if err != nil {
|
|
return "", nil, false, err
|
|
}
|
|
var ok []Build
|
|
for _, b := range all {
|
|
if b.Failed == "" && (b.ManifestDigest != "" || manifestBytes(b.Manifest) != nil) &&
|
|
(b.Commit == commit || strings.HasPrefix(b.Commit, commit)) {
|
|
ok = append(ok, b)
|
|
}
|
|
}
|
|
sortByRecorded(ok)
|
|
if len(ok) == 0 {
|
|
return "", nil, false, nil
|
|
}
|
|
raw, err := manifestOfRecord(ctx, c, chain.Build{ID: ok[0].ID, Manifest: ok[0].Manifest, ManifestDigest: ok[0].ManifestDigest})
|
|
return ok[0].ID, raw, err == nil, err
|
|
}
|
|
|
|
// chainManifestOf is the manifest a build was recorded with.
|
|
func (i *Inventory) chainManifestOf(ctx context.Context, c *chain.Chain, id string) ([]byte, error) {
|
|
rec, ok, err := i.chainRecordOf(ctx, c, id)
|
|
if err != nil || !ok {
|
|
return nil, err
|
|
}
|
|
return manifestOfRecord(ctx, c, rec)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// chainHeldMade is, per module, what the build standing for its newest successful build made. Read from the pointers: the
|
|
// module's `worked` (its newest success) and `held` (the first build of the run of one source that it stands on), one record
|
|
// of each. A build that failed its gate ends a run (issue 280), and the pointer cannot say where; then the module's builds
|
|
// are read.
|
|
func (i *Inventory) chainHeldMade(ctx context.Context, c *chain.Chain) (map[string][]Artifact, error) {
|
|
worked, err := i.pointedBuilds(ctx, c, "*.worked")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
held, err := i.pointedBuilds(ctx, c, "*.held")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
failed, err := i.failedAtAGate(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string][]Artifact{}
|
|
for module, w := range worked {
|
|
stand := w
|
|
if h, ok := held[module]; ok && !failed[w.ID] {
|
|
stand = h
|
|
}
|
|
if failed[stand.ID] && stand.ID != w.ID {
|
|
rows, _, err := i.chainSourceRows(ctx, c, module)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if builds := rows[module]; len(builds) > 0 {
|
|
var made []Artifact
|
|
if err := json.Unmarshal(builds[standing(builds, 0)].made, &made); err == nil {
|
|
out[module] = made
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
out[module] = fromChainBuild(stand).Made
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// LayoutReport is what the layout of the builds bucket did.
|
|
type LayoutReport struct {
|
|
// Records is the keys of before it read, Written the keys this run made (records, pointers, manifests) and Present the
|
|
// records already under their new key.
|
|
Records, Written, Present int
|
|
}
|
|
|
|
// ErrNotLaidOut is a read of the builds before the controller's start has laid them out under their modules (issue 1121):
|
|
// refused loudly, never answered from keys that are not there.
|
|
var ErrNotLaidOut = errors.New("the builds are not laid out under their modules yet; the controller's start does it and says so")
|
|
|
|
// LayBuildsOut lays the builds out under their modules once and writes the marker key (chain.LayoutMarkerKey) that lets them
|
|
// be read. It needs the chain attached and the lease held (the chain's writes ask), and is idempotent: with the marker it
|
|
// does nothing, and a run that stopped half way is finished by the next. The old keys stay (ADR 0408 §4).
|
|
func (i *Inventory) LayBuildsOut(ctx context.Context, say func(string, ...any)) error {
|
|
c := i.attached()
|
|
if c == nil {
|
|
return ErrNoChain
|
|
}
|
|
if _, found, err := c.Builds.GetRaw(ctx, chain.LayoutMarkerKey); err != nil {
|
|
return err
|
|
} else if found {
|
|
i.markLaidOut()
|
|
return nil
|
|
}
|
|
report, err := i.layoutBuilds(ctx, c, say)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body, _ := json.Marshal(map[string]any{"records": report.Records, "at": time.Now().UTC()})
|
|
if _, err := c.Builds.CreateRaw(ctx, chain.LayoutMarkerKey, body); err != nil && !errors.Is(err, chain.ErrExists) {
|
|
return fmt.Errorf("the builds are laid out and the marker is not written: %w", err)
|
|
}
|
|
i.markLaidOut()
|
|
return nil
|
|
}
|
|
|
|
// layoutBuilds lays the builds of the bucket out under their modules: each record of a key that is its id alone is written
|
|
// under `<module>.<id>` (a request, and a build that knew no module, under `req.<id>`), its manifest moved to the manifests
|
|
// bucket, and the pointers made. **Idempotent**: a key that exists is already moved, and a pointer that holds what it should
|
|
// is not written. The old keys stay (ADR 0408 §4) and are not read again.
|
|
func (i *Inventory) layoutBuilds(ctx context.Context, c *chain.Chain, say func(string, ...any)) (LayoutReport, error) {
|
|
if say == nil {
|
|
say = func(string, ...any) {}
|
|
}
|
|
var r LayoutReport
|
|
legacy, err := c.Builds.FilteredRaw(ctx, "*")
|
|
if err != nil {
|
|
return r, fmt.Errorf("the builds of before the layout cannot be read: %w", err)
|
|
}
|
|
ids := make([]string, 0, len(legacy))
|
|
for key := range legacy {
|
|
ids = append(ids, key)
|
|
}
|
|
sort.Strings(ids)
|
|
byModule := map[string][]chain.Build{}
|
|
manifestsDone := map[string]bool{}
|
|
for _, key := range ids {
|
|
k, err := chain.DecodeKept[chain.Build](legacy[key])
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
rec := k.Value
|
|
r.Records++
|
|
if r.Records%1000 == 0 {
|
|
say("laying the builds out: %d of %d records, %d keys written so far", r.Records, len(ids), r.Written)
|
|
}
|
|
// A manifest already written in this run is not written again.
|
|
digest := ""
|
|
if body := manifestBytes([]byte(rec.Manifest)); body != nil && c.Kept {
|
|
digest = chain.ManifestKey(body)
|
|
if !manifestsDone[digest] {
|
|
if _, err := c.Manifests.CreateRaw(ctx, digest, body); err == nil {
|
|
r.Written++
|
|
} else if !errors.Is(err, chain.ErrExists) {
|
|
return r, fmt.Errorf("the manifest of %s is not kept: %w", key, err)
|
|
}
|
|
manifestsDone[digest] = true
|
|
}
|
|
}
|
|
if digest != "" {
|
|
rec.Manifest, rec.ManifestDigest = nil, digest
|
|
}
|
|
target := chain.BuildKey(rec.Module, rec.ID)
|
|
if rec.State == chain.BuildAsked {
|
|
target = chain.RequestKey(rec.ID)
|
|
}
|
|
switch _, err := c.Builds.Create(ctx, target, rec); {
|
|
case err == nil:
|
|
r.Written++
|
|
case errors.Is(err, chain.ErrExists):
|
|
r.Present++
|
|
default:
|
|
return r, fmt.Errorf("%s is not written under %s: %w", key, target, err)
|
|
}
|
|
if rec.State != chain.BuildAsked {
|
|
wrote, err := i.pointTo(ctx, c, chain.IDKey(rec.ID), idPointer(rec.Module), false)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
if wrote {
|
|
r.Written++
|
|
}
|
|
}
|
|
if rec.State != chain.BuildAsked && rec.Module != "" && rec.Request != nil {
|
|
// The request beside its outcome, so the requests are read from their own prefix.
|
|
ans := chain.Build{ID: rec.ID, State: chain.BuildAnswered, Repository: rec.Repository, Ref: rec.Ref, Path: rec.Path,
|
|
Asked: rec.Asked, Request: rec.Request, Module: rec.Module, Failed: rec.Failed, At: rec.At}
|
|
switch _, err := c.Builds.Create(ctx, chain.RequestKey(rec.ID), ans); {
|
|
case err == nil:
|
|
r.Written++
|
|
case errors.Is(err, chain.ErrExists):
|
|
default:
|
|
return r, err
|
|
}
|
|
}
|
|
if rec.State != chain.BuildAsked && rec.Module != "" {
|
|
byModule[rec.Module] = append(byModule[rec.Module], rec)
|
|
}
|
|
}
|
|
for module, recs := range byModule {
|
|
points, err := pointsOf(module, recs)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
for _, p := range points {
|
|
wrote, err := i.pointTo(ctx, c, p.key, p.value, true)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
if wrote {
|
|
r.Written++
|
|
}
|
|
}
|
|
}
|
|
say("laid out the builds under their modules: %d records read, %d keys written, %d records already there", r.Records, r.Written, r.Present)
|
|
return r, nil
|
|
}
|