144 lines
6.1 KiB
Go
144 lines
6.1 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// OneNodePrefix opens the Step of a done build that a build module built on one node only, because no second node
|
|
// qualified (ADR 0478 clause 4); the build modules that did so follow in parentheses, then ": ". Claim reads it to build
|
|
// the key again, as a new revision, once a second node qualifies.
|
|
const OneNodePrefix = "one node ("
|
|
|
|
// OneNodeModules are the build modules a done build built on one node only, or none when it is not such a build.
|
|
func OneNodeModules(b delivery.BuilderBuild) []string {
|
|
if b.State != delivery.BuildDone || !strings.HasPrefix(b.Step, OneNodePrefix) {
|
|
return nil
|
|
}
|
|
list, _, ok := strings.Cut(strings.TrimPrefix(b.Step, OneNodePrefix), "): ")
|
|
if !ok || list == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(list, ", ")
|
|
}
|
|
|
|
// 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
|
|
// MoreNodes says whether one of these build modules, which built a done key on one node only, has a second node that
|
|
// qualifies now and the key may be built again (it is not held back after a failed second build, issue 1664). Nil means
|
|
// none does.
|
|
MoreNodes func(modules []string) bool
|
|
}
|
|
|
|
// 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 built on one node only (OneNodePrefix)
|
|
// when a second node qualifies now (ClaimOptions.MoreNodes): that is taken as it stands, with the done value, and its
|
|
// builder writes the new revision when the second build ends (ADR 0478 clause 4);
|
|
// - 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 yet (RefusedPrefix, ADR 0478 clause 2) or for digests that differ (DigestsDifferPrefix, clause 4),
|
|
// which wait for another fingerprint;
|
|
// - 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 modules := OneNodeModules(cur); len(modules) > 0 && opt.MoreNodes != nil && opt.MoreNodes(modules) {
|
|
return &Claim{Key: key, Rev: rev, Build: cur}, nil
|
|
}
|
|
return nil, nil
|
|
case cur.State == delivery.BuildFailed && (strings.HasPrefix(cur.Error, RefusedPrefix) || strings.HasPrefix(cur.Error, DigestsDifferPrefix)):
|
|
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.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
|
|
}
|