1304 lines
43 KiB
Go
1304 lines
43 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, SourceInputs: b.SourceInputs,
|
|
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, SourceInputs: c.SourceInputs,
|
|
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,
|
|
// What it was made from, which builds says (novox/hq issue 1208).
|
|
SourceFingerprint: b.SourceFingerprint})
|
|
}
|
|
}
|
|
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 record already under its new key is moved: a run again (the start before this one laid it out) neither reads its
|
|
// manifest nor writes it, and **no pointer is set back**: they are only made where missing.
|
|
own := chain.BuildKey(rec.Module, rec.ID)
|
|
if rec.State == chain.BuildAsked {
|
|
own = chain.RequestKey(rec.ID)
|
|
}
|
|
if _, there, err := c.Builds.GetRaw(ctx, own); err != nil {
|
|
return r, err
|
|
} else if there {
|
|
r.Present++
|
|
if rec.State != chain.BuildAsked {
|
|
if wrote, err := i.pointTo(ctx, c, chain.IDKey(rec.ID), idPointer(rec.Module), false); err != nil {
|
|
return r, err
|
|
} else if wrote {
|
|
r.Written++
|
|
}
|
|
}
|
|
if rec.State != chain.BuildAsked && rec.Module != "" {
|
|
byModule[rec.Module] = append(byModule[rec.Module], rec)
|
|
}
|
|
continue
|
|
}
|
|
// 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, false)
|
|
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
|
|
}
|