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
177 lines
8.2 KiB
Go
177 lines
8.2 KiB
Go
package assigngraph
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
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/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
|
)
|
|
|
|
type memObjects map[string][]byte
|
|
|
|
func (m memObjects) Put(_ context.Context, name string, data []byte) error {
|
|
m[name] = data
|
|
return nil
|
|
}
|
|
func (m memObjects) Get(_ context.Context, name string) ([]byte, error) {
|
|
d, ok := m[name]
|
|
if !ok {
|
|
return nil, ErrNoObject
|
|
}
|
|
return d, nil
|
|
}
|
|
func (m memObjects) Names(context.Context) ([]string, error) {
|
|
out := []string{}
|
|
for n := range m {
|
|
out = append(out, n)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func half(branch string, revision int64, entries ...meshcatalog.CatalogEntry) meshcatalogclient.Half {
|
|
h := meshcatalogclient.Half{Revision: meshcatalog.CatalogRevision{Kind: "trunk", Branch: branch, Revision: revision, Source: revision + 10},
|
|
Entries: map[string]meshcatalog.CatalogEntry{}}
|
|
for _, e := range entries {
|
|
h.Entries[e.Module] = e
|
|
h.Revision.Entries = append(h.Revision.Entries, meshcatalog.EntryRef{Module: e.Module, Digest: "sha256:" + e.Module + e.Fp})
|
|
}
|
|
return h
|
|
}
|
|
|
|
// Design 47 *Two graphs, one source of truth* (lines 1353-1366, 1416-1418) and *An acting part's new version acts first*
|
|
// (lines 1835-1836): each entry one module version on a node from the catalog graph of the branch its placement names,
|
|
// its edges provisioning and acting, a revision computed again from its inputs equal to the one kept.
|
|
func TestTheAssignmentGraphIsComputedFromItsInputsAndKept(t *testing.T) {
|
|
ctx := context.Background()
|
|
in := Inputs{
|
|
Halves: map[string]meshcatalogclient.Half{"main": half("main", 7,
|
|
meshcatalog.CatalogEntry{Module: "node-engine", Fp: "src2:e1", Acts: []meshcatalog.ActingRule{{Seat: "node-engine", Step: "compose", On: "every", Bound: "30m"}}},
|
|
meshcatalog.CatalogEntry{Module: "build-go", Fp: "src2:b1", Acts: []meshcatalog.ActingRule{{Seat: "node-build-go", Step: "build", On: "go"}}},
|
|
meshcatalog.CatalogEntry{Module: "postgres", Fp: "src2:p1", Provides: []string{"database"}},
|
|
meshcatalog.CatalogEntry{Module: "web", Fp: "src2:w1", Requires: []string{"database", "secrets"}, Toolchains: map[string]meshcatalog.BuildEdge{"go": {Version: "1"}}},
|
|
)},
|
|
Placements: map[string]mma.Assignments{
|
|
"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "node-engine"}, {Module: "postgres"}, {Module: "build-go"},
|
|
{Module: "web", Bindings: map[string]mma.Provider{"database": {Node: "anchor", Module: "postgres"}}}}},
|
|
"laptop": {Node: "laptop", Modules: []mma.PlacedModule{{Module: "node-engine"}, {Module: "web", Build: "src2:w0"},
|
|
{Module: "photos", Branch: "beta"}, {Module: "ghost"}}},
|
|
},
|
|
FirstBranch: func(module string) string {
|
|
if module == "ghost" {
|
|
return ""
|
|
}
|
|
return "main"
|
|
},
|
|
}
|
|
c := Compute(in)
|
|
byNode := map[string]meshwants.AssignmentEntry{}
|
|
for _, ref := range c.Revision.Entries {
|
|
byNode[ref.Node+"/"+ref.Module] = c.Entries[ref.Digest]
|
|
}
|
|
webA, webL := byNode["anchor/web"], byNode["laptop/web"]
|
|
if len(c.Revision.Entries) != 6 || len(c.Revision.Catalogs) != 1 || c.Revision.Catalogs[0] != (meshwants.CatalogOf{Branch: "main", Revision: 7, Source: 17}) {
|
|
t.Fatalf("revision %+v", c.Revision)
|
|
}
|
|
if webA.Branch != "main" || webA.Fp != "src2:w1" || webA.Catalog != "sha256:websrc2:w1" || webL.Fp != "src2:w0" {
|
|
t.Errorf("web's entries: %+v, %+v (the placement's build is the version on laptop)", webA, webL)
|
|
}
|
|
want := []meshwants.Edge{
|
|
{Kind: EdgeActing, Node: "anchor", Module: "build-go", Fp: "src2:b1", Seat: "node-build-go", Step: "build"},
|
|
{Kind: EdgeActing, Node: "anchor", Module: "node-engine", Fp: "src2:e1", Seat: "node-engine", Step: "compose", Bound: "30m"},
|
|
{Kind: EdgeProvisioning, Node: "anchor", Module: "postgres", Fp: "src2:p1", Provision: "database"},
|
|
}
|
|
if fmt.Sprint(webA.Edges) != fmt.Sprint(want) {
|
|
t.Errorf("web on anchor's edges:\n got %+v\nwant %+v", webA.Edges, want)
|
|
}
|
|
// laptop's web: its own node's node-engine composes it, the build module placed once builds it, and no binding or pin
|
|
// names a provider there.
|
|
if e := webL.Edges; len(e) != 2 || e[0].Module != "build-go" || e[0].Node != "anchor" || e[1].Module != "node-engine" || e[1].Node != "laptop" {
|
|
t.Errorf("web on laptop's edges: %+v", e)
|
|
}
|
|
if e := byNode["anchor/postgres"].Edges; len(e) != 1 || e[0].Module != "node-engine" {
|
|
t.Errorf("postgres builds no Go, so only the node-engine acts on it: %+v", e)
|
|
}
|
|
if len(byNode["anchor/node-engine"].Edges) != 0 {
|
|
t.Errorf("an acting part acts on itself: %+v", byNode["anchor/node-engine"].Edges)
|
|
}
|
|
if strings.Join(c.Uncomputed, "|") != "laptop/ghost: no watched branch: its placement names none and no registration of it is known|"+
|
|
"laptop/photos: the catalog graph of the branch beta is not held" {
|
|
t.Errorf("uncomputed %q", c.Uncomputed)
|
|
}
|
|
|
|
// Kept once; computed again from the same inputs it is the same revision, kept no second time.
|
|
o := memObjects{}
|
|
at := time.Date(2026, 11, 10, 18, 0, 0, 0, time.UTC)
|
|
rev, kept, err := Keep(ctx, o, c, "catalog-revision", at)
|
|
if err != nil || !kept || rev.Revision != 1 || rev.Cause != "catalog-revision" || len(o) != 7 {
|
|
t.Fatalf("kept %v %+v (%v), objects %d", kept, rev, err, len(o))
|
|
}
|
|
if again, kept, err := Keep(ctx, o, Compute(in), "placement-changed", at); err != nil || kept || again.Revision != 1 {
|
|
t.Errorf("recomputed from the same inputs: kept %v, %+v, %v", kept, again, err)
|
|
}
|
|
// A placement change moves laptop's web to the catalog's version: revision 2, one entry more stored, the rest shared.
|
|
in.Placements["laptop"].Modules[1].Build = ""
|
|
rev2, kept, err := Keep(ctx, o, Compute(in), "placement-changed", at)
|
|
if err != nil || !kept || rev2.Revision != 2 || len(o) != 9 || rev2.Placements == rev.Placements {
|
|
t.Fatalf("revision 2 %v %+v (%v), objects %d", kept, rev2, err, len(o))
|
|
}
|
|
|
|
// The verbs read only what is kept.
|
|
r, err := Revision(ctx, o, meshwants.RevisionInput{})
|
|
if err != nil || r.Revision.Revision != 2 {
|
|
t.Errorf("revision = %+v, %v", r, err)
|
|
}
|
|
if _, err := Revision(ctx, o, meshwants.RevisionInput{Group: "g", Heads: "h"}); !errors.Is(err, ErrNoCandidate) {
|
|
t.Errorf("a candidate's revision: %v", err)
|
|
}
|
|
e, err := Entry(ctx, o, meshwants.EntryInput{Revision: 1, Node: "laptop", Module: "web"})
|
|
if err != nil || e.Entry.Fp != "src2:w0" || !strings.HasPrefix(e.Digest, "sha256:") {
|
|
t.Errorf("entry = %+v, %v", e, err)
|
|
}
|
|
d, err := Difference(ctx, o, meshwants.DifferenceInput{From: 1, To: 2})
|
|
if err != nil || len(d.Changes) != 1 || fmt.Sprint(d.Changes[0]) != fmt.Sprint(meshwants.Change{Node: "laptop", Module: "web", From: "src2:w0", To: "src2:w1"}) ||
|
|
strings.Join(d.Nodes, ",") != "laptop" {
|
|
t.Errorf("difference = %+v, %v", d, err)
|
|
}
|
|
if _, err := Difference(ctx, o, meshwants.DifferenceInput{To: 2}); err == nil {
|
|
t.Error("a difference with one revision answered")
|
|
}
|
|
if ev := Event(rev2); ev.Revision != 2 || len(ev.Catalogs) != 1 || ev.Kind != "trunk" {
|
|
t.Errorf("event %+v", ev)
|
|
}
|
|
}
|
|
|
|
// Design 47 line 1385: a whole assignment graph revision of about 120 modules on four nodes is under the bus's 256 KiB
|
|
// value limit.
|
|
func TestARevisionOf120ModulesOnFourNodesFitsOneValue(t *testing.T) {
|
|
var entries []meshcatalog.CatalogEntry
|
|
placements := map[string]mma.Assignments{}
|
|
for i := range 120 {
|
|
entries = append(entries, meshcatalog.CatalogEntry{Module: fmt.Sprintf("module-%03d", i), Fp: "src2:" + strings.Repeat("a", 64)})
|
|
}
|
|
for _, node := range []string{"anchor", "home", "desk", "laptop"} {
|
|
a := mma.Assignments{Node: node}
|
|
for _, e := range entries {
|
|
a.Modules = append(a.Modules, mma.PlacedModule{Module: e.Module})
|
|
}
|
|
placements[node] = a
|
|
}
|
|
h := half("main", 1, entries...)
|
|
for i := range h.Revision.Entries {
|
|
h.Revision.Entries[i].Digest = "sha256:" + strings.Repeat("b", 64)
|
|
}
|
|
c := Compute(Inputs{Halves: map[string]meshcatalogclient.Half{"main": h}, Placements: placements, FirstBranch: func(string) string { return "main" }})
|
|
raw, _ := json.Marshal(c.Revision)
|
|
if len(c.Revision.Entries) != 480 || len(raw) >= 256<<10 {
|
|
t.Errorf("%d entries in %d bytes", len(c.Revision.Entries), len(raw))
|
|
}
|
|
}
|