A run's candidate catalog-revision (meshcatalogclient's candidates hook) is read whole through the catalog seat's revision verb; the assignment graph is computed from it in place of its branch's half and kept apart from trunk's under candidate.<group>.<heads>, the group run-<id>. revision, entry and difference serve it (a candidate against trunk). The runner's group-ended of the run drops it and writes no want; the gate's heads-replaced drops nothing; its entries stay while it is kept. Builds: design 47 G §189, §190, §186, §187, §359, A-201, A-235, A-160 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
139 lines
4.9 KiB
Go
139 lines
4.9 KiB
Go
package assigngraph
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
|
|
)
|
|
|
|
// A candidate's assignment graph (design 47 G §189, §190, A-201, A-235): computed per run on request from the candidate's catalog
|
|
// graph revision, kept apart from trunk's under the seat's key form candidate.<group>.<heads>, the group `run-<id>`, and dropped on
|
|
// the runner's group-ended of that run; it writes no want, and the gate's heads-replaced drops nothing.
|
|
|
|
// CandidateObject is the object name of a candidate's revision (the seat's key form candidate.<group>.<heads>).
|
|
func CandidateObject(group, heads string) string {
|
|
return meshwants.AssignmentRevisionsByCandidateGroupHeadsKey(group, heads)
|
|
}
|
|
|
|
// KeepCandidate keeps c as the candidate's revision of group and heads, numbered after the one kept of it, unless that one lists
|
|
// the same entries from the same inputs; its entry objects are written first, an entry trunk shares stored once.
|
|
func KeepCandidate(ctx context.Context, o Objects, c Computed, group, heads, cause string, at time.Time) (meshwants.AssignmentRevision, bool, error) {
|
|
if group == "" || heads == "" {
|
|
return meshwants.AssignmentRevision{}, false, fmt.Errorf("a candidate's revision names its group and its heads, not %q and %q", group, heads)
|
|
}
|
|
was, err := readCandidateObject(ctx, o, CandidateObject(group, heads))
|
|
has := err == nil
|
|
if err != nil && !errors.Is(err, ErrNoObject) {
|
|
return was, false, err
|
|
}
|
|
if has && Same(was, c.Revision) {
|
|
return was, false, nil
|
|
}
|
|
rev := c.Revision
|
|
rev.Kind, rev.Group, rev.Heads, rev.Cause = meshcatalogclient.KindCandidate, group, heads, cause
|
|
rev.Revision = was.Revision + 1
|
|
t := at.UTC()
|
|
rev.At = &t
|
|
if err := writeEntries(ctx, o, c, rev); err != nil {
|
|
return was, false, err
|
|
}
|
|
list, err := json.Marshal(rev)
|
|
if err != nil {
|
|
return was, false, err
|
|
}
|
|
if err := o.Put(ctx, CandidateObject(group, heads), list); err != nil {
|
|
return was, false, fmt.Errorf("the candidate %s's revision in %s: %w", group, RevisionsObjects, err)
|
|
}
|
|
return rev, true, nil
|
|
}
|
|
|
|
// ReadCandidate is the candidate's revision kept of group, of heads when named, else the newest kept of the group; ErrNoCandidate
|
|
// when none is kept.
|
|
func ReadCandidate(ctx context.Context, o Objects, group, heads string) (meshwants.AssignmentRevision, error) {
|
|
if heads != "" {
|
|
rev, err := readCandidateObject(ctx, o, CandidateObject(group, heads))
|
|
if errors.Is(err, ErrNoObject) {
|
|
err = fmt.Errorf("%w (group %q, heads %q)", ErrNoCandidate, group, heads)
|
|
}
|
|
return rev, err
|
|
}
|
|
names, err := CandidateNames(ctx, o, group)
|
|
if err != nil {
|
|
return meshwants.AssignmentRevision{}, err
|
|
}
|
|
var newest meshwants.AssignmentRevision
|
|
found := false
|
|
for _, n := range names {
|
|
rev, err := readCandidateObject(ctx, o, n)
|
|
if err != nil {
|
|
return rev, err
|
|
}
|
|
if !found || rev.At != nil && newest.At != nil && rev.At.After(*newest.At) {
|
|
newest, found = rev, true
|
|
}
|
|
}
|
|
if !found {
|
|
return newest, fmt.Errorf("%w (group %q)", ErrNoCandidate, group)
|
|
}
|
|
return newest, nil
|
|
}
|
|
|
|
// CandidateNames is the object name of every revision kept of the candidate group; every candidate's when group is "".
|
|
func CandidateNames(ctx context.Context, o Objects, group string) ([]string, error) {
|
|
names, err := o.Names(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the objects %s: %w", RevisionsObjects, err)
|
|
}
|
|
var out []string
|
|
for _, n := range names {
|
|
g, _, ok := meshwants.ParseAssignmentRevisionsByCandidateGroupHeadsKey(n)
|
|
if ok && (group == "" || g == group) {
|
|
out = append(out, n)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Remover is an object bucket that also removes an object.
|
|
type Remover interface {
|
|
Objects
|
|
// Delete removes the object of that name; one not held is no error.
|
|
Delete(ctx context.Context, name string) error
|
|
}
|
|
|
|
// DropCandidate removes every revision kept of the candidate group (its run's group-ended, A-235); the entries it alone listed are
|
|
// removed with the next revisions nothing pins. It says how many it removed.
|
|
func DropCandidate(ctx context.Context, o Remover, group string) (int, error) {
|
|
if strings.TrimSpace(group) == "" {
|
|
return 0, errors.New("a candidate to drop names its group")
|
|
}
|
|
names, err := CandidateNames(ctx, o, group)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
for i, n := range names {
|
|
if err := o.Delete(ctx, n); err != nil {
|
|
return i, fmt.Errorf("the candidate %s's revision %s: %w", group, n, err)
|
|
}
|
|
}
|
|
return len(names), nil
|
|
}
|
|
|
|
func readCandidateObject(ctx context.Context, o Objects, name string) (meshwants.AssignmentRevision, error) {
|
|
var rev meshwants.AssignmentRevision
|
|
data, err := o.Get(ctx, name)
|
|
if err != nil {
|
|
return rev, err
|
|
}
|
|
if err := json.Unmarshal(data, &rev); err != nil {
|
|
return rev, fmt.Errorf("the candidate's revision %s: %w", name, err)
|
|
}
|
|
return rev, nil
|
|
}
|