Files
mesh-delivery/modules/mesh-delivery-assigner/internal/assigngraph/verbs.go
T
jschoubben 08d38f651b Merge branch 'feat/47-G-fix11-b5j' into feat/47-G-join11
# Conflicts:
#	modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go
2026-11-12 10:37:21 +01:00

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
}