VW2: the assigner's difference fills Tiers and each change's after
assigngraph.Compare (verbs.go:80, new order at verbs.go:112) orders the changes by the edges of their entries: each change names the changes it is ordered after with the edge's kind (meshwants.Change.after, seat.json, regenerated), and Tiers holds them in that order, so the view orders nothing itself. Builds: design 47 G §512, §186, §378, A-242
This commit is contained in:
@@ -174,3 +174,56 @@ func TestARevisionOf120ModulesOnFourNodesFitsOneValue(t *testing.T) {
|
||||
t.Errorf("%d entries in %d bytes", len(c.Revision.Entries), len(raw))
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §512, §186, A-242: the difference orders its changes itself: each change names the changes it is ordered after
|
||||
// with the kind of the edge, and Tiers holds the changes in that order, an acting part and a provider before what they act on.
|
||||
func TestTheDifferenceFillsItsTiersAndEachChangesAfter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
build := func(ne, pg, web string) Computed {
|
||||
return Compute(Inputs{
|
||||
Halves: map[string]meshcatalogclient.Half{"main": half("main", 7,
|
||||
meshcatalog.CatalogEntry{Module: "node-engine", Fp: ne, Acts: []meshcatalog.ActingRule{{Seat: "node-engine", Step: "compose", On: "every"}}},
|
||||
meshcatalog.CatalogEntry{Module: "postgres", Fp: pg, Provides: []string{"database"}},
|
||||
meshcatalog.CatalogEntry{Module: "web", Fp: web, Requires: []string{"database"}},
|
||||
)},
|
||||
Placements: map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "node-engine"}, {Module: "postgres"},
|
||||
{Module: "web", Bindings: map[string]mma.Provider{"database": {Node: "anchor", Module: "postgres"}}}}}},
|
||||
FirstBranch: func(string) string { return "main" },
|
||||
})
|
||||
}
|
||||
o := memObjects{}
|
||||
at := time.Date(2026, 11, 12, 1, 0, 0, 0, time.UTC)
|
||||
if _, _, err := Keep(ctx, o, build("src2:e1", "src2:p1", "src2:w1"), "catalog-revision", at); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := Keep(ctx, o, build("src2:e2", "src2:p2", "src2:w2"), "catalog-revision", at); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d, err := Difference(ctx, o, meshwants.DifferenceInput{From: 1, To: 2})
|
||||
if err != nil || len(d.Changes) != 3 {
|
||||
t.Fatalf("difference %+v, %v", d, err)
|
||||
}
|
||||
after := map[string]string{}
|
||||
for _, c := range d.Changes {
|
||||
var a []string
|
||||
for _, x := range c.After {
|
||||
a = append(a, x.Kind+":"+x.Node+"/"+x.Module)
|
||||
}
|
||||
after[c.Module] = strings.Join(a, ",")
|
||||
}
|
||||
if after["node-engine"] != "" || after["postgres"] != "acting:anchor/node-engine" ||
|
||||
after["web"] != "acting:anchor/node-engine,provisioning:anchor/postgres" {
|
||||
t.Errorf("after = %v", after)
|
||||
}
|
||||
var tiers []string
|
||||
for _, tier := range d.Tiers {
|
||||
var ms []string
|
||||
for _, r := range tier {
|
||||
ms = append(ms, r.Module)
|
||||
}
|
||||
tiers = append(tiers, strings.Join(ms, ","))
|
||||
}
|
||||
if strings.Join(tiers, "|") != "node-engine|postgres|web" {
|
||||
t.Errorf("tiers = %v", tiers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
|
||||
)
|
||||
@@ -94,6 +95,7 @@ func Compare(ctx context.Context, o Objects, from, to meshwants.AssignmentRevisi
|
||||
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]
|
||||
@@ -110,11 +112,95 @@ func Compare(ctx context.Context, o Objects, from, to meshwants.AssignmentRevisi
|
||||
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 {
|
||||
|
||||
@@ -517,8 +517,18 @@ type CatalogOf struct {
|
||||
Source int64 `json:"source,omitempty"`
|
||||
}
|
||||
|
||||
// ChangeAfterItem is a type of the contract.
|
||||
type ChangeAfterItem struct {
|
||||
// the kind of the edge that orders the change after that one: provisioning, contribution or acting
|
||||
Kind string `json:"kind"`
|
||||
Module string `json:"module"`
|
||||
Node string `json:"node"`
|
||||
}
|
||||
|
||||
// Change is one module on a node whose version differs between two revisions.
|
||||
type Change struct {
|
||||
// the other changes of the difference it is ordered after, each with its edge's kind (design 47 G section 512)
|
||||
After []ChangeAfterItem `json:"after,omitempty"`
|
||||
// the edges that moved
|
||||
Edges []Edge `json:"edges,omitempty"`
|
||||
// the fingerprint before; absent when it is new there
|
||||
|
||||
@@ -955,6 +955,30 @@
|
||||
"$ref": "#/defs/Edge"
|
||||
},
|
||||
"description": "the edges that moved"
|
||||
},
|
||||
"after": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string"
|
||||
},
|
||||
"module": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "the kind of the edge that orders the change after that one: provisioning, contribution or acting"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node",
|
||||
"module",
|
||||
"kind"
|
||||
]
|
||||
},
|
||||
"description": "the other changes of the difference it is ordered after, each with its edge's kind (design 47 G section 512)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -186,8 +186,17 @@ export interface CatalogOf {
|
||||
"source"?: number;
|
||||
}
|
||||
|
||||
export interface ChangeAfterItem {
|
||||
/** the kind of the edge that orders the change after that one: provisioning, contribution or acting */
|
||||
"kind": string;
|
||||
"module": string;
|
||||
"node": string;
|
||||
}
|
||||
|
||||
/** one module on a node whose version differs between two revisions */
|
||||
export interface Change {
|
||||
/** the other changes of the difference it is ordered after, each with its edge's kind (design 47 G section 512) */
|
||||
"after"?: ChangeAfterItem[];
|
||||
/** the edges that moved */
|
||||
"edges"?: Edge[];
|
||||
/** the fingerprint before; absent when it is new there */
|
||||
|
||||
Reference in New Issue
Block a user