# Conflicts: # modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go
259 lines
8.1 KiB
Go
259 lines
8.1 KiB
Go
package assigngraph
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
|
|
)
|
|
|
|
// ErrNoCandidate answers a verb asked of a candidate's revision the assigner keeps none of: a run's candidate is kept from its
|
|
// catalog graph revision until its run's group-ended (A-201, A-235).
|
|
var ErrNoCandidate = errors.New("the assigner keeps no such candidate's assignment graph revision")
|
|
|
|
// ErrNoRevision answers a verb asked while no trunk revision is kept yet.
|
|
var ErrNoRevision = errors.New("no assignment graph revision is kept yet")
|
|
|
|
func revisionOf(ctx context.Context, o Objects, revision int64, group, heads string) (meshwants.AssignmentRevision, error) {
|
|
if group != "" || heads != "" {
|
|
if group == "" {
|
|
return meshwants.AssignmentRevision{}, fmt.Errorf("a candidate's revision is asked by its group, with or without its heads, not by heads %q alone", heads)
|
|
}
|
|
rev, err := ReadCandidate(ctx, o, group, heads)
|
|
if err == nil && revision > 0 && rev.Revision != revision {
|
|
err = fmt.Errorf("%w (group %q, revision %d: the one kept is %d)", ErrNoCandidate, group, revision, rev.Revision)
|
|
}
|
|
return rev, err
|
|
}
|
|
if revision > 0 {
|
|
return ReadRevision(ctx, o, revision)
|
|
}
|
|
rev, has, err := Newest(ctx, o)
|
|
if err == nil && !has {
|
|
err = ErrNoRevision
|
|
}
|
|
return rev, err
|
|
}
|
|
|
|
// Revision answers the seat's verb revision: one kept revision, the trunk's newest when none is named.
|
|
func Revision(ctx context.Context, o Objects, in meshwants.RevisionInput) (meshwants.RevisionAnswer, error) {
|
|
rev, err := revisionOf(ctx, o, in.Revision, in.Group, in.Heads)
|
|
return meshwants.RevisionAnswer{Revision: rev}, err
|
|
}
|
|
|
|
// Entry answers the seat's verb entry: one entry whole, by its digest, or by its node and module in a revision (the
|
|
// trunk's newest when none is named); an acting edge names the seat whose rule gave it.
|
|
func Entry(ctx context.Context, o Objects, in meshwants.EntryInput) (meshwants.EntryAnswer, error) {
|
|
digest := in.Digest
|
|
if digest == "" {
|
|
if in.Node == "" || in.Module == "" {
|
|
return meshwants.EntryAnswer{}, errors.New("entry takes a digest, or a node and a module")
|
|
}
|
|
rev, err := revisionOf(ctx, o, in.Revision, in.Group, in.Heads)
|
|
if err != nil {
|
|
return meshwants.EntryAnswer{}, err
|
|
}
|
|
for _, ref := range rev.Entries {
|
|
if ref.Node == in.Node && ref.Module == in.Module {
|
|
digest = ref.Digest
|
|
}
|
|
}
|
|
if digest == "" {
|
|
return meshwants.EntryAnswer{}, fmt.Errorf("revision %d has no entry of %s on %s", rev.Revision, in.Module, in.Node)
|
|
}
|
|
}
|
|
e, err := ReadEntry(ctx, o, digest)
|
|
return meshwants.EntryAnswer{Digest: digest, Entry: e}, err
|
|
}
|
|
|
|
// Difference answers the seat's verb difference: two kept revisions compared, each module on a node whose version
|
|
// differs, with the edges that moved, and the nodes it touches.
|
|
func Difference(ctx context.Context, o Objects, in meshwants.DifferenceInput) (meshwants.Difference, error) {
|
|
if in.Group != "" || in.Heads != "" {
|
|
// A candidate against trunk (§186, §187): from the trunk revision named, else the newest kept.
|
|
to, err := revisionOf(ctx, o, 0, in.Group, in.Heads)
|
|
if err != nil {
|
|
return meshwants.Difference{}, err
|
|
}
|
|
from, err := revisionOf(ctx, o, in.From, "", "")
|
|
if err != nil {
|
|
return meshwants.Difference{}, err
|
|
}
|
|
return Compare(ctx, o, from, to)
|
|
}
|
|
if in.From <= 0 || in.To <= 0 {
|
|
return meshwants.Difference{}, errors.New("difference takes the two kept revisions it compares, from and to")
|
|
}
|
|
from, err := ReadRevision(ctx, o, in.From)
|
|
if err != nil {
|
|
return meshwants.Difference{}, err
|
|
}
|
|
to, err := ReadRevision(ctx, o, in.To)
|
|
if err != nil {
|
|
return meshwants.Difference{}, err
|
|
}
|
|
return Compare(ctx, o, from, to)
|
|
}
|
|
|
|
// Compare is the graph difference of two revisions.
|
|
func Compare(ctx context.Context, o Objects, from, to meshwants.AssignmentRevision) (meshwants.Difference, error) {
|
|
d := meshwants.Difference{From: from.Revision, To: to.Revision, Changes: []meshwants.Change{}}
|
|
was, err := entriesOf(ctx, o, from)
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
now, err := entriesOf(ctx, o, to)
|
|
if err != nil {
|
|
return d, err
|
|
}
|
|
nodes := map[string]bool{}
|
|
edges := map[string][]meshwants.Edge{}
|
|
for _, k := range slices.Sorted(maps.Keys(union(was, now))) {
|
|
a, hadA := was[k]
|
|
b, hasB := now[k]
|
|
if hadA && hasB && a.Fp == b.Fp {
|
|
continue
|
|
}
|
|
c := meshwants.Change{}
|
|
if hadA {
|
|
c.Node, c.Module, c.From = a.Node, a.Module, a.Fp
|
|
}
|
|
if hasB {
|
|
c.Node, c.Module, c.To = b.Node, b.Module, b.Fp
|
|
}
|
|
c.Edges = moved(a.Edges, b.Edges)
|
|
d.Changes = append(d.Changes, c)
|
|
nodes[c.Node] = true
|
|
if hasB {
|
|
edges[c.Node+"/"+c.Module] = b.Edges
|
|
} else {
|
|
edges[c.Node+"/"+c.Module] = a.Edges
|
|
}
|
|
}
|
|
d.Nodes = slices.Sorted(maps.Keys(nodes))
|
|
order(&d, edges, digests(from, to))
|
|
return d, nil
|
|
}
|
|
|
|
// digests are the entry digests of the two revisions by node and module, the newer's winning.
|
|
func digests(from, to meshwants.AssignmentRevision) map[string]string {
|
|
out := map[string]string{}
|
|
for _, r := range []meshwants.AssignmentRevision{from, to} {
|
|
for _, ref := range r.Entries {
|
|
out[ref.Node+"/"+ref.Module] = ref.Digest
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// order fills each change's After and the difference's Tiers (design 47 G §512, §186, A-242): a change is ordered after
|
|
// each other change of the difference that an edge of its entry names (the module version it is provisioned by, contributes
|
|
// to or is acted on by), with that edge's kind; its tier is one past the highest tier it is after. Changes on a cycle come in
|
|
// the last tier. The view orders nothing itself.
|
|
func order(d *meshwants.Difference, edges map[string][]meshwants.Edge, digest map[string]string) {
|
|
if len(d.Changes) == 0 {
|
|
return
|
|
}
|
|
in := map[string]int{}
|
|
for i, c := range d.Changes {
|
|
in[c.Node+"/"+c.Module] = i
|
|
}
|
|
for i, c := range d.Changes {
|
|
self := c.Node + "/" + c.Module
|
|
seen := map[string]bool{}
|
|
for _, e := range edges[self] {
|
|
k := e.Node + "/" + e.Module
|
|
if _, ok := in[k]; !ok || k == self || seen[e.Kind+k] {
|
|
continue
|
|
}
|
|
seen[e.Kind+k] = true
|
|
d.Changes[i].After = append(d.Changes[i].After, meshwants.ChangeAfterItem{Node: e.Node, Module: e.Module, Kind: e.Kind})
|
|
}
|
|
slices.SortFunc(d.Changes[i].After, func(a, b meshwants.ChangeAfterItem) int {
|
|
return strings.Compare(a.Kind+"\x00"+a.Node+"\x00"+a.Module, b.Kind+"\x00"+b.Node+"\x00"+b.Module)
|
|
})
|
|
}
|
|
tier := map[int]int{}
|
|
var level func(int, map[int]bool) int
|
|
level = func(i int, visiting map[int]bool) int {
|
|
if t, ok := tier[i]; ok {
|
|
return t
|
|
}
|
|
if visiting[i] {
|
|
return -1
|
|
}
|
|
visiting[i] = true
|
|
t := 0
|
|
for _, a := range d.Changes[i].After {
|
|
l := level(in[a.Node+"/"+a.Module], visiting)
|
|
if l < 0 {
|
|
return -1
|
|
}
|
|
t = max(t, l+1)
|
|
}
|
|
delete(visiting, i)
|
|
tier[i] = t
|
|
return t
|
|
}
|
|
top := 0
|
|
for i := range d.Changes {
|
|
top = max(top, level(i, map[int]bool{}))
|
|
}
|
|
for i := range d.Changes {
|
|
if _, ok := tier[i]; !ok {
|
|
tier[i] = top + 1
|
|
}
|
|
}
|
|
for i, c := range d.Changes {
|
|
for len(d.Tiers) <= tier[i] {
|
|
d.Tiers = append(d.Tiers, nil)
|
|
}
|
|
d.Tiers[tier[i]] = append(d.Tiers[tier[i]], meshwants.NodeEntryRef{Node: c.Node, Module: c.Module, Digest: digest[c.Node+"/"+c.Module]})
|
|
}
|
|
d.Tiers = slices.DeleteFunc(d.Tiers, func(t []meshwants.NodeEntryRef) bool { return len(t) == 0 })
|
|
}
|
|
|
|
func entriesOf(ctx context.Context, o Objects, rev meshwants.AssignmentRevision) (map[string]meshwants.AssignmentEntry, error) {
|
|
out := make(map[string]meshwants.AssignmentEntry, len(rev.Entries))
|
|
for _, ref := range rev.Entries {
|
|
e, err := ReadEntry(ctx, o, ref.Digest)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("revision %d: %w", rev.Revision, err)
|
|
}
|
|
out[ref.Node+"/"+ref.Module] = e
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func union(a, b map[string]meshwants.AssignmentEntry) map[string]bool {
|
|
out := map[string]bool{}
|
|
for k := range a {
|
|
out[k] = true
|
|
}
|
|
for k := range b {
|
|
out[k] = true
|
|
}
|
|
return out
|
|
}
|
|
|
|
// moved are the edges in one list and not the other.
|
|
func moved(a, b []meshwants.Edge) []meshwants.Edge {
|
|
var out []meshwants.Edge
|
|
for _, e := range b {
|
|
if !slices.Contains(a, e) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
for _, e := range a {
|
|
if !slices.Contains(b, e) {
|
|
out = append(out, e)
|
|
}
|
|
}
|
|
sortEdges(out)
|
|
return out
|
|
}
|