A key done on one node before R4e is not built again for a second; a key failed for differing digests stays as it is.
207 lines
9.3 KiB
Go
207 lines
9.3 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery/buildrun"
|
|
)
|
|
|
|
// ErrConflict is what a Store answers when a Create finds the key or an Update finds another revision than the one given.
|
|
var ErrConflict = errors.New("the key exists or changed")
|
|
|
|
// Store is the builder's bucket (builds): create and compare-and-set, each answering the key's new revision. Only the
|
|
// builder's credential writes it (ADR 0429 clause 5).
|
|
type Store interface {
|
|
Get(ctx context.Context, key string) (value []byte, revision uint64, found bool, err error)
|
|
Create(ctx context.Context, key string, value []byte) (uint64, error)
|
|
Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error)
|
|
// Keys are every key of the bucket.
|
|
Keys(ctx context.Context) ([]string, error)
|
|
}
|
|
|
|
// Claimer claims fingerprints for the builder instance By (ADR 0429 clause 5: a builder claims a fingerprint by creating
|
|
// its key and moves it by compare-and-set).
|
|
type Claimer struct {
|
|
Store Store
|
|
By string
|
|
}
|
|
|
|
// Claim is a build the builder holds: its key, the revision the next write must be made at and the value last written.
|
|
type Claim struct {
|
|
Key string
|
|
Rev uint64
|
|
Build delivery.BuilderBuild
|
|
}
|
|
|
|
// ClaimOptions say what the builder knows about the moment of a claim.
|
|
type ClaimOptions struct {
|
|
// TakeOver takes a key found building whoever holds it: the builder has just started and looks at the key for the first
|
|
// time, and a build left building by a builder that is gone, on this node or another, is nobody's (ADR 0478 clause 6).
|
|
TakeOver bool
|
|
// ImageBuilder says whether a node runs a healthy image build module now: a key failed for NoImageBuilderPrefix is taken
|
|
// again only then. Nil means none does.
|
|
ImageBuilder func() bool
|
|
// Trunk is whether the trunk's record names this fingerprint. A done key whose image or package has no reference was
|
|
// built as a candidate's, which never pushes or publishes (ADR 0487 clause 12, ADR 0498 clauses 5 and 6); on the trunk it
|
|
// is taken again to publish them, an image only when an image builder runs (ImageBuilder).
|
|
Trunk bool
|
|
// Published says whether a package version `<name>@<version>` is published: one of the builder's done keys names it as
|
|
// a package's reference. A key queued waiting for a version (BuilderBuild.Waits) is taken only once it is (ADR 0500
|
|
// clause 6). Nil means none is.
|
|
Published func(ref string) bool
|
|
}
|
|
|
|
// Unpublished is whether a done build holds an image built without a push or a package built without a publish: it has
|
|
// no reference (a candidate's, ADR 0487 clause 12, ADR 0498 clauses 5 and 6).
|
|
func Unpublished(b delivery.BuilderBuild) bool { return len(UnpublishedModules(b)) > 0 }
|
|
|
|
// UnpublishedModules are the build modules that publish what a done build holds unpublished: the image build module for
|
|
// an image, the Node build module for a package; sorted, none when it holds nothing unpublished.
|
|
func UnpublishedModules(b delivery.BuilderBuild) []string {
|
|
if b.State != delivery.BuildDone {
|
|
return nil
|
|
}
|
|
var image, pkg bool
|
|
for _, a := range b.Artifacts {
|
|
image = image || (a.Kind == catalogue.ArtifactImage && a.Reference == "")
|
|
pkg = pkg || (a.Kind == catalogue.ArtifactPackage && a.Reference == "")
|
|
}
|
|
var out []string
|
|
if image {
|
|
out = append(out, ImageBuildModule)
|
|
}
|
|
if pkg {
|
|
out = append(out, buildrun.NodeBuildModule)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// publishable are the build modules of UnpublishedModules that can publish now: the image build module only while a node
|
|
// runs it (imageBuilder).
|
|
func publishable(b delivery.BuilderBuild, imageBuilder func() bool) []string {
|
|
var out []string
|
|
for _, bm := range UnpublishedModules(b) {
|
|
if bm != ImageBuildModule || (imageBuilder != nil && imageBuilder()) {
|
|
out = append(out, bm)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Unreferenced is whether a done build holds a bundle, an archive or an upstream copy that names no reference: a key done
|
|
// before R4e, whose artifacts stayed on their build nodes (ADR 0498 clause 8). An artifact of no kind (a key written before
|
|
// kinds) is one too. Claim takes such a key again, once: its new revision names them.
|
|
func Unreferenced(b delivery.BuilderBuild) bool {
|
|
if b.State != delivery.BuildDone {
|
|
return false
|
|
}
|
|
for _, a := range b.Artifacts {
|
|
if a.Reference == "" && (a.Kind == "" || stored(a.Kind)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Claim takes the build of module at fingerprint, or answers nil when it is not this builder's to build now:
|
|
// - no key: it is created building, by this builder; a create that finds the key made meanwhile gets nothing;
|
|
// - a done key is not taken again (the same source is never built twice), except one whose bundle, archive or upstream
|
|
// copy names no reference (Unreferenced, a key done before R4e: ADR 0498 clause 8), which is claimed building as a new
|
|
// revision and built whole, and one whose image or package was built unpublished as a candidate's once the trunk
|
|
// names it (ClaimOptions.Trunk): that is taken as it stands, with the done value, and its builder writes the new
|
|
// revision when the publishing build ends (ADR 0498 clause 6). From R4e one node builds each fingerprint (ADR 0478
|
|
// clause 4), so a key built on one node is never built again for a second;
|
|
// - a failed key is taken again as a new revision of the same key (ADR 0429 clause 18), except one failed for a kind no
|
|
// builder builds, for an upstream pinned by no digest or a package version taken (delivery.FailedUntilChanged: ADR 0478
|
|
// clause 2, ADR 0498 clauses 4 and 5) or for digests that differ (buildrun.DigestsDifferPrefix, clause 4),
|
|
// which wait for another fingerprint, and one failed for want of an image builder (NoImageBuilderPrefix), which waits for
|
|
// another fingerprint or a node that runs the image build module (ClaimOptions.ImageBuilder);
|
|
// - a key queued waiting for a package version (Waits) is claimed building as a new revision once that version is
|
|
// published (ClaimOptions.Published, ADR 0500 clause 6), and not before;
|
|
// - a key left queued or building by this builder is taken as it stands (ADR 0478 clause 5: a stopped builder takes its
|
|
// own again when it starts), and so is one left by another builder when ClaimOptions.TakeOver (clause 6).
|
|
func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string, opt ClaimOptions) (*Claim, error) {
|
|
key, err := delivery.BuilderBuildKey(module, fingerprint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
building := delivery.BuilderBuild{State: delivery.BuildBuilding, By: c.By, Step: "claimed", Manifest: manifest}
|
|
value, rev, found, err := c.Store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", key, err)
|
|
}
|
|
if !found {
|
|
return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Create(ctx, key, data) })
|
|
}
|
|
cur, err := delivery.DecodeBuilderBuild(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s holds what is not a build: %w", key, err)
|
|
}
|
|
switch {
|
|
case cur.State == delivery.BuildDone:
|
|
if Unreferenced(cur) {
|
|
return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) })
|
|
}
|
|
if opt.Trunk && len(publishable(cur, opt.ImageBuilder)) > 0 {
|
|
return &Claim{Key: key, Rev: rev, Build: cur}, nil
|
|
}
|
|
return nil, nil
|
|
case cur.State == delivery.BuildFailed && (delivery.FailedUntilChanged(cur.Error) || strings.HasPrefix(cur.Error, buildrun.DigestsDifferPrefix)):
|
|
return nil, nil
|
|
case cur.State == delivery.BuildFailed && strings.HasPrefix(cur.Error, NoImageBuilderPrefix) && (opt.ImageBuilder == nil || !opt.ImageBuilder()):
|
|
return nil, nil
|
|
case cur.State == delivery.BuildFailed:
|
|
return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) })
|
|
case cur.State == delivery.BuildQueued && cur.Waits != "":
|
|
if opt.Published == nil || !opt.Published(cur.Waits) {
|
|
return nil, nil // the package it waits for is not published yet
|
|
}
|
|
return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) })
|
|
case cur.By == c.By || opt.TakeOver:
|
|
return &Claim{Key: key, Rev: rev, Build: cur}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (c Claimer) write(_ context.Context, key string, b delivery.BuilderBuild, put func([]byte) (uint64, error)) (*Claim, error) {
|
|
data, err := delivery.EncodeBuilderBuild(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rev, err := put(data)
|
|
if errors.Is(err, ErrConflict) {
|
|
return nil, nil // another builder made it first
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", key, err)
|
|
}
|
|
b.V = delivery.Version
|
|
return &Claim{Key: key, Rev: rev, Build: b}, nil
|
|
}
|
|
|
|
// Finish moves the claimed key to b by compare-and-set at the claim's revision, and writes nothing when b is the value
|
|
// the key holds. A key that changed meanwhile is ErrConflict: the claim is no longer this builder's.
|
|
func (c Claimer) Finish(ctx context.Context, cl *Claim, b delivery.BuilderBuild) error {
|
|
b.V = delivery.Version
|
|
if delivery.SameBuilderBuild(cl.Build, b) {
|
|
return nil
|
|
}
|
|
data, err := delivery.EncodeBuilderBuild(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rev, err := c.Store.Update(ctx, cl.Key, data, cl.Rev)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", cl.Key, err)
|
|
}
|
|
cl.Rev, cl.Build = rev, b
|
|
return nil
|
|
}
|