108 lines
4.0 KiB
Go
108 lines
4.0 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 never taken again (the same source is never built twice);
|
|
// - 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), which waits 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); one held by another builder is left, since nothing reclaims without presence.
|
|
func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string) (*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:
|
|
return nil, nil
|
|
case cur.State == delivery.BuildFailed && strings.HasPrefix(cur.Error, RefusedPrefix):
|
|
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:
|
|
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
|
|
}
|