Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/claim.go
T

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
}