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 } // 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. 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 }