Files
mesh-delivery/modules/mesh-delivery-assigner/internal/assigngraph/compute.go
T
jschoubben 05494e7bb9 W5.1, W5.2, W5.5: the assignment graph computed per branch, kept as objects, and served by mesh-wants' verbs
Design 47 at hq main b229f5c4, lines 1353-1366 (each entry one module version
on a node, its edges provisioning and acting; each revision naming its inputs,
kept as objects in the old section with its cap, served by the seat's verbs),
1385-1388 (under 256 KiB for 120 modules on four nodes; a 512 MiB cap), 1416-1418
and 522-525 (a node's entries from the catalog graph of the branch its placement
names, by default the registration's first), 909-914 (the placement's build is
the version), 1835-1836 (an edge from the acting part's version to each version
its rule names), and the Reverted table (assignment graph revisions as objects,
form 1).

internal/assigngraph computes a revision from the halves held per branch, the
placements and the capability readings; keeps it numbered after the newest
when it differs, each entry once by its digest; and answers revision, entry
and difference from the kept objects only. The manifest serves the three verbs
and declares the objects assignment-revisions. A placed module whose entry
cannot be computed is named in Uncomputed, never left out in silence.

Not built here (the design does not say, or held): contribution edges;
provisioning to a provider no binding or pin names; tiers; a candidate's
revision (W5.9); removal of a revision no record pins (pins are W5.9's).
The process does not compute and keep yet: that needs the registrations'
first view and a catalog half per branch (handover).

Milestone 47/G, issue 1966. Tests: TestTheAssignmentGraphIsComputedFromItsInputsAndKept,
TestARevisionOf120ModulesOnFourNodesFitsOneValue, TestTheSeatsVerbs/1953/D4-D6
(failed before), TestManifest's objects row (failed with the old manifest).

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ

Builds: design 47 G §173, §175, §176, §180, §194, §209, §223, §263, §346
2026-11-11 14:43:17 +01:00

207 lines
7.9 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/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/keys"
"mesh-delivery-assigner/internal/assignment"
)
// The kinds of an edge of the assignment graph (ADR 0656; the seat's Edge).
const (
EdgeProvisioning = "provisioning"
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
}
// 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
}
// 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
}
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, 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})
}
for _, r := range ce.Requires {
prov, ok := m.Bindings[r]
if !ok {
prov, ok = in.Placements[node].Pins[r]
}
if ok {
p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: prov.Node, Module: prov.Module, Provision: r})
}
}
placed = append(placed, p)
branches[branch] = true
}
}
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
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.Equal(a.Capabilities, b.Capabilities)
}