Files
mesh-delivery/modules/mesh-delivery-assigner/internal/assigngraph/compute.go
T
jschoubben 644aa8b46c Merge branch 'feat/47-G-fix11-b4b' into feat/47-G-join11
# Conflicts:
#	modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go
2026-11-12 10:36:56 +01:00

303 lines
12 KiB
Go

// Package assigngraph is the assigner's assignment graph (design 47, *Two graphs, one source of truth*; ADR 0656): the
// module versions placed on nodes, with the edges between versions, computed from the catalog graph of the branch each
// placement names, the placements and the nodes' capability readings, and kept per revision as objects of the seat
// mesh-wants' kept data assignment-revisions.
package assigngraph
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"maps"
"slices"
"strings"
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"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"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"mesh-delivery-assigner/internal/assignment"
)
// The kinds of an edge of the assignment graph (ADR 0656; the seat's Edge).
const (
EdgeProvisioning = "provisioning"
EdgeContribution = "contribution"
EdgeActing = "acting"
)
// Inputs are what a revision is computed from, and nothing else (design 47 lines 1353-1356).
type Inputs struct {
// Halves is the catalog graph revision held of each watched branch, by branch.
Halves map[string]meshcatalogclient.Half
// Placements is every node's placement key, by node.
Placements map[string]mma.Assignments
// FirstBranch is the first watched branch of a module's registration, the branch a placement naming none runs
// (design 47 lines 522-523); "" when no registration is known for it.
FirstBranch func(module string) string
// Capabilities is each node's capability reading, by its digest.
Capabilities []meshwants.AssignmentRevisionCapabilitiesItem
// Definitions are the seat definitions read whole through the seat mesh-catalog's verb seat-definitions, by seat: of
// them, those an entry's catalog graph revision names in its definitions say which seat delivers a requirement (§708).
Definitions map[string]seatdef.Definition
}
// Computed is one assignment graph revision as computed, before it is numbered and kept.
type Computed struct {
Revision meshwants.AssignmentRevision
// Entries is every entry the revision lists, by its digest.
Entries map[string]meshwants.AssignmentEntry
// Uncomputed is each placed module whose entry could not be computed, as "<node>/<module>: why", sorted: it is said,
// never left out in silence.
Uncomputed []string
// Cycles are the cycles among the waits within one step (Cycles, design 47 G §272): said, never left in silence.
Cycles []string
}
// placedAt is one placed module with what its entry is computed from.
type placedAt struct {
node string
placed mma.PlacedModule
branch string
entry meshwants.AssignmentEntry
langs map[string]bool
acts []actingRule
// provides is what its catalog entry provides; contributes the requirements it carries contributed values to (§709).
provides, contributes []string
// delivering is, by requirement, the seat that delivers it among the definitions its revision names (§708).
delivering map[string]seatdef.Definition
// optional says a requirement is one it runs without (ADR 0330 §3), by the entry's manifest (A-229).
optional func(string) bool
}
type actingRule struct{ seat, step, on, bound string }
// Compute is the assignment graph revision of in: one entry per module placed on a node whose branch's catalog graph
// holds it, its version the build the placement names or else the catalog entry's fingerprint (design 47 lines 909-914),
// with the edges of provisioning to the providers its placement binds or its node pins, else the provider on its node or
// the only provider (an entry held naming the candidates where none is), and of acting from each acting
// part's version to each version its rule names (design 47 *An acting part's new version acts first*: "the assigner
// gives an edge from the acting part's version to each version its rule names").
func Compute(in Inputs) Computed {
out := Computed{Entries: map[string]meshwants.AssignmentEntry{}}
var placed []*placedAt
branches := map[string]bool{}
for _, node := range slices.Sorted(maps.Keys(in.Placements)) {
mods := slices.Clone(in.Placements[node].Modules)
slices.SortFunc(mods, func(a, b mma.PlacedModule) int { return strings.Compare(a.Module, b.Module) })
for _, m := range mods {
branch := m.Branch
if branch == "" && in.FirstBranch != nil {
branch = in.FirstBranch(m.Module)
}
if branch == "" {
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: no watched branch: its placement names none and no registration of it is known", node, m.Module))
continue
}
half, ok := in.Halves[branch]
if !ok {
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: the catalog graph of the branch %s is not held", node, m.Module, branch))
continue
}
ce, ok := half.Entries[m.Module]
if !ok {
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: the catalog graph revision %d of %s holds no entry of it", node, m.Module, half.Revision.Revision, branch))
continue
}
digest := ""
for _, ref := range half.Revision.Entries {
if ref.Module == m.Module {
digest = ref.Digest
}
}
if digest == "" {
out.Uncomputed = append(out.Uncomputed, fmt.Sprintf("%s/%s: the catalog graph revision %d of %s lists no digest of its entry", node, m.Module, half.Revision.Revision, branch))
continue
}
fp := ce.Fp
if m.Build != "" {
fp = m.Build
}
p := &placedAt{node: node, placed: m, branch: branch, langs: map[string]bool{},
entry: meshwants.AssignmentEntry{V: keys.Version, Node: node, Module: m.Module, Branch: branch, Fp: fp, Catalog: digest}}
for lang := range ce.Toolchains {
p.langs[lang] = true
}
for _, r := range ce.Acts {
p.acts = append(p.acts, actingRule{seat: r.Seat, step: r.Step, on: r.On, bound: r.Bound})
}
p.provides, p.contributes = ce.Provides, ce.Contributes
p.delivering = map[string]seatdef.Definition{}
for _, d := range half.Revision.Definitions {
if def, ok := in.Definitions[d.Seat]; ok && def.Delivers != "" {
if _, had := p.delivering[def.Delivers]; !had {
p.delivering[def.Delivers] = def
}
}
}
p.optional = func(string) bool { return false }
if man, err := manifestOf(ce); err == nil {
p.optional = man.IsOptional
}
placed = append(placed, p)
branches[branch] = true
}
}
// Each requirement points, by design 27's order (design 47 G §357, §708), to the named seat's holder, else a pin, else
// the provider on the consumer's node, else the only provider; otherwise the entry is held, naming the candidates, and
// nothing is guessed. A requirement contributes names gets a contribution edge to the same provider (§709).
holders := assignment.SeatHoldersOf(in.Placements)
for _, p := range placed {
ce := in.Halves[p.branch].Entries[p.placed.Module]
for _, r := range ce.Requires {
prov, held := resolve(p, r, placed, holders, in.Placements[p.node].Pins)
switch {
case held != nil:
p.entry.Held = append(p.entry.Held, *held)
case prov.Module != "":
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: prov.Node, Module: prov.Module, Provision: r})
if slices.Contains(p.contributes, r) {
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeContribution, Node: prov.Node, Module: prov.Module, Provision: r})
}
}
}
slices.SortFunc(p.entry.Held, func(a, b meshwants.Held) int { return strings.Compare(a.Requirement, b.Requirement) })
}
at := map[string]*placedAt{}
for _, p := range placed {
at[p.node+"/"+p.placed.Module] = p
}
for _, p := range placed {
for i, e := range p.entry.Edges {
if prov, ok := at[e.Node+"/"+e.Module]; ok {
p.entry.Edges[i].Fp = prov.entry.Fp
}
}
}
// Acting: an acting part on the entry's own node acts there (a node's seat is held on every node); else every node it
// is placed on (a seat held once for the mesh is held on one).
byModule := map[string][]*placedAt{}
for _, p := range placed {
byModule[p.placed.Module] = append(byModule[p.placed.Module], p)
}
for _, part := range slices.Sorted(maps.Keys(byModule)) {
holders := byModule[part]
for _, rule := range holders[0].acts {
for _, t := range placed {
if t.placed.Module == part || (rule.on != "" && rule.on != "every" && !t.langs[rule.on]) {
continue
}
from := holders
for _, h := range holders {
if h.node == t.node {
from = []*placedAt{h}
}
}
for _, h := range from {
t.entry.Edges = append(t.entry.Edges, meshwants.Edge{Kind: EdgeActing, Node: h.node, Module: part, Fp: h.entry.Fp,
Seat: rule.seat, Step: rule.step, Bound: rule.bound})
}
}
}
}
rev := meshwants.AssignmentRevision{V: keys.Version, Kind: meshcatalogclient.KindTrunk, Placements: assignment.ExportDigest(in.Placements),
Capabilities: in.Capabilities, Entries: []meshwants.NodeEntryRef{}}
for _, b := range slices.Sorted(maps.Keys(branches)) {
h := in.Halves[b]
rev.Catalogs = append(rev.Catalogs, meshwants.CatalogOf{Branch: b, Revision: h.Revision.Revision, Source: h.Revision.Source})
}
for _, p := range placed {
sortEdges(p.entry.Edges)
d := EntryDigest(p.entry)
out.Entries[d] = p.entry
rev.Entries = append(rev.Entries, meshwants.NodeEntryRef{Node: p.node, Module: p.placed.Module, Digest: d})
}
out.Revision = rev
out.Cycles = Cycles(slices.Collect(maps.Values(out.Entries)))
slices.Sort(out.Uncomputed)
return out
}
func sortEdges(edges []meshwants.Edge) {
slices.SortFunc(edges, func(a, b meshwants.Edge) int {
return strings.Compare(edgeKey(a), edgeKey(b))
})
}
func edgeKey(e meshwants.Edge) string {
return strings.Join([]string{e.Kind, e.Node, e.Module, e.Provision, e.Seat, e.Step, e.Fp, e.Bound}, "\x00")
}
// EntryDigest is the digest an entry is stored under: the same entry, the same digest.
func EntryDigest(e meshwants.AssignmentEntry) string {
raw, _ := json.Marshal(e)
sum := sha256.Sum256(raw)
return "sha256:" + hex.EncodeToString(sum[:])
}
// Same says two revisions list the same entries computed from the same inputs, so the second is no new revision.
func Same(a, b meshwants.AssignmentRevision) bool {
return slices.Equal(a.Entries, b.Entries) && slices.Equal(a.Catalogs, b.Catalogs) && a.Placements == b.Placements &&
slices.EqualFunc(a.Capabilities, b.Capabilities, func(x, y meshwants.AssignmentRevisionCapabilitiesItem) bool {
// A reading's digest is over all of it: its readings and its runner's fingerprint among it.
return x.Node == y.Node && x.Digest == y.Digest
})
}
// resolve is the provider requirement r of p points to by §708's order, or the hold that says why none is: none and no
// hold for an optional requirement nothing provides (ADR 0330 §3).
func resolve(p *placedAt, r string, placed []*placedAt, holders map[string]mma.SeatHolders, pins map[string]mma.Provider) (mma.Provider, *meshwants.Held) {
// 1. The named seat's holder, from the placements' holds as the state seat-holders keeps them; a delivering seat no
// placement holds holds the entry, and no other step is tried.
if def, ok := p.delivering[r]; ok {
node := ""
if def.Scope == seatdef.ScopeNode {
node = p.node
}
key := def.Seat
if node != "" {
key += "." + node
}
if sh := holders[key]; len(sh.Holders) > 0 {
return mma.Provider{Node: sh.Holders[0].Node, Module: sh.Holders[0].Module}, nil
}
return mma.Provider{}, &meshwants.Held{Requirement: r, Seat: def.Seat, Candidates: []meshwants.HeldCandidate{}}
}
// 2. A pin: the placed module's bindings, else its node's pins.
if prov, ok := p.placed.Bindings[r]; ok {
return prov, nil
}
if prov, ok := pins[r]; ok {
return prov, nil
}
// 3. The provider on the node; 4. the only provider.
var here, all []*placedAt
for _, q := range placed {
if q != p && slices.Contains(q.provides, r) {
all = append(all, q)
if q.node == p.node {
here = append(here, q)
}
}
}
switch {
case len(here) == 1:
return mma.Provider{Node: here[0].node, Module: here[0].placed.Module}, nil
case len(here) == 0 && len(all) == 1:
return mma.Provider{Node: all[0].node, Module: all[0].placed.Module}, nil
case len(all) == 0 && p.optional(r):
return mma.Provider{}, nil
}
h := &meshwants.Held{Requirement: r, Candidates: []meshwants.HeldCandidate{}}
for _, q := range all {
h.Candidates = append(h.Candidates, meshwants.HeldCandidate{Node: q.node, Module: q.placed.Module})
}
slices.SortFunc(h.Candidates, func(a, b meshwants.HeldCandidate) int {
return strings.Compare(a.Node+"/"+a.Module, b.Node+"/"+b.Module)
})
return mma.Provider{}, h
}