mesh/delivery-group group fix/1279-consumers-from-now rejected: a member's own check failed
mesh/merge-gate waits for its group's composed check
mesh/repo-check head 677f94e9012b merged into trunk 1325c8b452c8 beside mesh-controller#400@a0c971ce — fail: its merge-check.sh failed: --- FAIL: TestBridg…
mesh/delivery superseded: a newer head of the same pull request
231 lines
8.0 KiB
Go
231 lines
8.0 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// ErrConflict is what a Store says when a write found the key changed or created since it was read.
|
|
var ErrConflict = errors.New("the key changed since it was read")
|
|
|
|
// ErrUnreadable wraps what says a message cannot be indexed, ever: it is never tried again.
|
|
var ErrUnreadable = errors.New("unreadable message")
|
|
|
|
// Store is the candidates bucket: a compare-and-set key-value store.
|
|
type Store interface {
|
|
// Get is the key's value and revision; found is false when there is none.
|
|
Get(ctx context.Context, key string) (value []byte, revision uint64, found bool, err error)
|
|
// Create writes a key that must not exist (ErrConflict when it does).
|
|
Create(ctx context.Context, key string, value []byte) error
|
|
// Update writes the key if its revision is still revision (ErrConflict when not).
|
|
Update(ctx context.Context, key string, value []byte, revision uint64) error
|
|
// Delete removes the key if its revision is still revision (ErrConflict when not).
|
|
Delete(ctx context.Context, key string, revision uint64) error
|
|
// Keys is every key the store holds now; none is an empty list, not an error.
|
|
Keys(ctx context.Context) ([]string, error)
|
|
}
|
|
|
|
// trunkLike are the branches that are no group: a pull request from one is nobody's delivery group (the rule of
|
|
// mesh-catalog modules/mesh-delivery/cmd/mesh-delivery/holder.go:321).
|
|
var trunkLike = map[string]bool{"main": true, "master": true, "trunk": true, "": true}
|
|
|
|
// writeTries bounds the reads again after a conflict; a message that still conflicts is left to the server to deliver again.
|
|
const writeTries = 8
|
|
|
|
// Indexer writes candidates.<group> from the forge bridge's messages.
|
|
type Indexer struct {
|
|
Store Store // candidates
|
|
// Modules is the modules bucket; nil writes no fingerprints.
|
|
Modules Store
|
|
Builds *Builds
|
|
Logf func(format string, args ...any)
|
|
}
|
|
|
|
func (ix *Indexer) logf(format string, args ...any) {
|
|
if ix.Logf != nil {
|
|
ix.Logf(format, args...)
|
|
}
|
|
}
|
|
|
|
// Apply takes one message of the bridge's stream FORGE into the group of its branch. A pull message adds or moves the
|
|
// pull request, a closed one or a merged message takes it out; a group left with none is deleted. A change is written
|
|
// by compare-and-set on the revision read, and read again on a conflict. A lone pull request is a group of one. A
|
|
// merged message also writes the fingerprint of each module of its repository (modules.go).
|
|
func (ix *Indexer) Apply(ctx context.Context, f delivery.Forge) error {
|
|
if f.Kind == delivery.ForgeOpen {
|
|
return ix.applyOpen(ctx, f)
|
|
}
|
|
if f.Kind != delivery.ForgePull && f.Kind != delivery.ForgeMerged {
|
|
return nil
|
|
}
|
|
if f.Kind == delivery.ForgeMerged && ix.Modules != nil {
|
|
if err := ix.applyMerged(ctx, f); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if trunkLike[f.Branch] {
|
|
ix.logf("%s#%d: its branch %q is no delivery group, so no candidate", f.Repository, f.Number, f.Branch)
|
|
return nil
|
|
}
|
|
key, err := delivery.CandidatesKey(f.Branch)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s#%d: %v", ErrUnreadable, f.Repository, f.Number, err)
|
|
}
|
|
return write(ctx, ix.Store, key, func(value []byte, found bool) ([]byte, bool, error) {
|
|
var cur *delivery.Candidate
|
|
if found {
|
|
c, err := delivery.DecodeCandidate(value)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err)
|
|
}
|
|
cur = &c
|
|
}
|
|
next := ix.next(cur, f)
|
|
switch {
|
|
case next == nil && cur == nil, next != nil && cur != nil && delivery.SameCandidate(*cur, *next):
|
|
return nil, false, nil
|
|
case next == nil:
|
|
return nil, true, nil
|
|
}
|
|
data, err := delivery.EncodeCandidate(*next)
|
|
return data, false, err
|
|
})
|
|
}
|
|
|
|
// applyOpen takes an open message into the candidates (novox/hq ADR 0443 §5): every pull request it does not name is
|
|
// taken out of its group, and a group left with none is deleted. What the message names is left as it is. A pull request
|
|
// opened after the bridge read the forge is heard after this message and added again by its own.
|
|
func (ix *Indexer) applyOpen(ctx context.Context, f delivery.Forge) error {
|
|
named := make(map[delivery.OpenPull]bool, len(f.Open))
|
|
for _, p := range f.Open {
|
|
named[p] = true
|
|
}
|
|
keys, err := ix.Store.Keys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("listing the candidates: %w", err)
|
|
}
|
|
for _, key := range keys {
|
|
err := write(ctx, ix.Store, key, func(value []byte, found bool) ([]byte, bool, error) {
|
|
if !found {
|
|
return nil, false, nil
|
|
}
|
|
c, err := delivery.DecodeCandidate(value)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err)
|
|
}
|
|
kept := slices.DeleteFunc(slices.Clone(c.Pulls), func(p delivery.CandidatePull) bool {
|
|
return !named[delivery.OpenPull{Repository: p.Repository, Number: p.Number}]
|
|
})
|
|
switch {
|
|
case len(kept) == len(c.Pulls):
|
|
return nil, false, nil
|
|
case len(kept) == 0:
|
|
ix.logf("%s: no pull request of the group is open, so no candidate", key)
|
|
return nil, true, nil
|
|
}
|
|
heads := make([]delivery.Head, len(kept))
|
|
for i, p := range kept {
|
|
heads[i] = p.Head
|
|
}
|
|
hash, err := delivery.HeadsHash(heads)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("%w: candidates.%s: %v", ErrUnreadable, key, err)
|
|
}
|
|
c.Pulls, c.HeadsHash = kept, hash
|
|
data, err := delivery.EncodeCandidate(c)
|
|
return data, false, err
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// write changes one key of a store by compare-and-set: compute is given the value read (found says there is one) and
|
|
// answers the new value, or del to remove the key, or neither for no change. A conflict reads again, up to writeTries.
|
|
func write(ctx context.Context, st Store, key string, compute func(value []byte, found bool) (next []byte, del bool, err error)) error {
|
|
for try := 0; try < writeTries; try++ {
|
|
value, revision, found, err := st.Get(ctx, key)
|
|
if err != nil {
|
|
return fmt.Errorf("reading %s: %w", key, err)
|
|
}
|
|
next, del, err := compute(value, found)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case next == nil && !del:
|
|
return nil
|
|
case del:
|
|
err = st.Delete(ctx, key, revision)
|
|
case !found:
|
|
err = st.Create(ctx, key, next)
|
|
default:
|
|
err = st.Update(ctx, key, next, revision)
|
|
}
|
|
if errors.Is(err, ErrConflict) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("writing %s: %w", key, err)
|
|
}
|
|
return nil
|
|
}
|
|
return fmt.Errorf("%s changed under every one of %d tries", key, writeTries)
|
|
}
|
|
|
|
// next is the group after the message; nil when the group is left with no pull request.
|
|
func (ix *Indexer) next(cur *delivery.Candidate, f delivery.Forge) *delivery.Candidate {
|
|
var pulls []delivery.CandidatePull
|
|
var base uint64
|
|
if cur != nil {
|
|
pulls, base = slices.Clone(cur.Pulls), cur.Base
|
|
}
|
|
at := slices.IndexFunc(pulls, func(p delivery.CandidatePull) bool { return p.Repository == f.Repository && p.Number == f.Number })
|
|
if f.Kind == delivery.ForgeMerged || f.Action == delivery.PullClosed {
|
|
if at < 0 {
|
|
return cur
|
|
}
|
|
pulls = slices.Delete(pulls, at, at+1)
|
|
} else {
|
|
entry := delivery.CandidatePull{Head: delivery.Head{Repository: f.Repository, Number: f.Number, Head: f.Head}, Modules: []string{}}
|
|
switch {
|
|
case f.DirsKnown:
|
|
for _, dir := range f.Dirs {
|
|
if m := ix.Builds.ModuleOf(f.Repository, dir); !slices.Contains(entry.Modules, m) {
|
|
entry.Modules = append(entry.Modules, m)
|
|
}
|
|
}
|
|
case at >= 0 && pulls[at].Head.Head == f.Head:
|
|
// Its directories are not known now, for the head the group already holds: what it knew stands.
|
|
entry.Modules = pulls[at].Modules
|
|
}
|
|
if at >= 0 {
|
|
pulls[at] = entry
|
|
} else {
|
|
pulls = append(pulls, entry)
|
|
}
|
|
}
|
|
if len(pulls) == 0 {
|
|
return nil
|
|
}
|
|
slices.SortFunc(pulls, func(a, b delivery.CandidatePull) int {
|
|
if c := strings.Compare(a.Repository, b.Repository); c != 0 {
|
|
return c
|
|
}
|
|
return a.Number - b.Number
|
|
})
|
|
heads := make([]delivery.Head, len(pulls))
|
|
for i, p := range pulls {
|
|
heads[i] = p.Head
|
|
}
|
|
hash, _ := delivery.HeadsHash(heads)
|
|
return &delivery.Candidate{V: delivery.Version, Group: f.Branch, Base: base, HeadsHash: hash, Pulls: pulls}
|
|
}
|