diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go index 2825d6d5..fca2b7fb 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/compute.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/compute.go @@ -58,13 +58,16 @@ type placedAt struct { entry meshwants.AssignmentEntry langs map[string]bool acts []actingRule + // provides is what its catalog entry provides; open its requirements its placement neither binds nor its node pins. + provides, open []string } 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 +// 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 { @@ -115,6 +118,7 @@ func Compute(in Inputs) Computed { 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 = ce.Provides for _, r := range ce.Requires { prov, ok := m.Bindings[r] if !ok { @@ -122,12 +126,42 @@ func Compute(in Inputs) Computed { } if ok { p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: prov.Node, Module: prov.Module, Provision: r}) + } else { + p.open = append(p.open, r) } } placed = append(placed, p) branches[branch] = true } } + // A requirement neither bound nor pinned points, by design 27's order (design 47 G §357), to the provider on the + // consumer's node, else to the only provider; otherwise the entry is held, naming the candidates, and nothing is guessed. + for _, p := range placed { + for _, r := range p.open { + 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: + p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: here[0].node, Module: here[0].placed.Module, Provision: r}) + case len(here) == 0 && len(all) == 1: + p.entry.Edges = append(p.entry.Edges, meshwants.Edge{Kind: EdgeProvisioning, Node: all[0].node, Module: all[0].placed.Module, Provision: r}) + default: + h := meshwants.Held{Provision: r} + for _, q := range all { + h.Candidates = append(h.Candidates, meshwants.Edge{Kind: EdgeProvisioning, Node: q.node, Module: q.placed.Module, Fp: q.entry.Fp, Provision: r}) + } + sortEdges(h.Candidates) + p.entry.Held = append(p.entry.Held, h) + } + } + } at := map[string]*placedAt{} for _, p := range placed { at[p.node+"/"+p.placed.Module] = p diff --git a/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go b/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go index 767c88e6..2827b7f6 100644 --- a/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go +++ b/modules/mesh-delivery-assigner/internal/assigngraph/graph_test.go @@ -91,11 +91,16 @@ func TestTheAssignmentGraphIsComputedFromItsInputsAndKept(t *testing.T) { 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" { + // laptop's web: its own node's node-engine composes it, the build module placed once builds it, and with no binding or + // pin its database is the only provider's (design 47 G §357). + if e := webL.Edges; len(e) != 3 || e[0].Module != "build-go" || e[0].Node != "anchor" || e[1].Module != "node-engine" || e[1].Node != "laptop" || + e[2] != (meshwants.Edge{Kind: EdgeProvisioning, Node: "anchor", Module: "postgres", Fp: "src2:p1", Provision: "database"}) { t.Errorf("web on laptop's edges: %+v", e) } + // Its secrets no placed module provides: held on both nodes, naming no candidate. + if h := webA.Held; len(h) != 1 || h[0].Provision != "secrets" || len(h[0].Candidates) != 0 { + t.Errorf("web on anchor is held on %+v", h) + } 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) } @@ -149,6 +154,63 @@ func TestTheAssignmentGraphIsComputedFromItsInputsAndKept(t *testing.T) { } } +// Design 47 G §357 and A-167: a requirement neither bound nor pinned points to the provider on the consumer's node, else +// to the only provider; otherwise the entry is held, naming the candidates, and nothing is guessed. +func TestARequirementPointsByTheOrderOrIsHeld(t *testing.T) { + h := half("main", 3, + meshcatalog.CatalogEntry{Module: "kv", Fp: "src2:k1", Provides: []string{"cache"}}, + meshcatalog.CatalogEntry{Module: "web", Fp: "src2:w1", Requires: []string{"cache"}}) + for _, c := range []struct { + name string + kv []string // the nodes kv is placed on + pin bool // web's node pins kv on laptop + edge string // the node of the provisioning edge, "" for none + held string // the candidates' nodes of the hold, "-" for no hold + }{ + {name: "the provider on the consumer's node", kv: []string{"anchor", "laptop"}, edge: "anchor", held: "-"}, + {name: "the only provider", kv: []string{"laptop"}, edge: "laptop", held: "-"}, + {name: "a pin before the node's own", kv: []string{"anchor", "laptop"}, pin: true, edge: "laptop", held: "-"}, + {name: "two providers elsewhere: held", kv: []string{"laptop", "server"}, held: "laptop,server"}, + {name: "none: held naming none", held: ""}, + } { + t.Run(c.name, func(t *testing.T) { + pl := map[string]mma.Assignments{"anchor": {Node: "anchor", Modules: []mma.PlacedModule{{Module: "web"}}}} + if c.pin { + pl["anchor"] = mma.Assignments{Node: "anchor", Modules: pl["anchor"].Modules, Pins: map[string]mma.Provider{"cache": {Node: "laptop", Module: "kv"}}} + } + for _, n := range c.kv { + a := pl[n] + a.Node, a.Modules = n, append(a.Modules, mma.PlacedModule{Module: "kv"}) + pl[n] = a + } + out := Compute(Inputs{Halves: map[string]meshcatalogclient.Half{"main": h}, Placements: pl, FirstBranch: func(string) string { return "main" }}) + var web meshwants.AssignmentEntry + for _, ref := range out.Revision.Entries { + if ref.Node == "anchor" && ref.Module == "web" { + web = out.Entries[ref.Digest] + } + } + edge := "" + for _, e := range web.Edges { + if e.Kind == EdgeProvisioning && e.Provision == "cache" { + edge = e.Node + } + } + held := "-" + if len(web.Held) == 1 && web.Held[0].Provision == "cache" { + var nodes []string + for _, cand := range web.Held[0].Candidates { + nodes = append(nodes, cand.Node) + } + held = strings.Join(nodes, ",") + } + if edge != c.edge || held != c.held || len(web.Held) > 1 { + t.Errorf("web points to %q, held on %q (%+v); want %q, %q", edge, held, web.Held, c.edge, c.held) + } + }) + } +} + // 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) { diff --git a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go index 23b263fc..e73a7cb0 100644 --- a/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go +++ b/modules/mesh-delivery-contracts/go/seats/meshwants/contract.go @@ -472,7 +472,9 @@ type AssignmentEntry struct { Catalog string `json:"catalog"` Edges []Edge `json:"edges,omitempty"` // the fingerprint it is to run - Fp string `json:"fp"` + Fp string `json:"fp"` + // the requirements no step of the order resolves (design 47 G section 357): the entry is held, naming the candidates, and nothing is guessed + Held []Held `json:"held,omitempty"` Module string `json:"module"` Node string `json:"node"` // the layout version @@ -582,6 +584,13 @@ type Fill struct { Store string `json:"store"` } +// Held is one requirement an entry is held on, for want of a provider. +type Held struct { + // a provisioning edge to each placed provider of it, none chosen; none when no placed module provides it + Candidates []Edge `json:"candidates,omitempty"` + Provision string `json:"provision"` +} + // NodeEntryRef is a type of the contract. type NodeEntryRef struct { // the entry's digest, its object's name diff --git a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json index e0564718..bdb5915b 100644 --- a/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json +++ b/modules/mesh-delivery-contracts/seats/mesh-wants/seat.json @@ -825,6 +825,13 @@ "items": { "$ref": "#/defs/Edge" } + }, + "held": { + "type": "array", + "description": "the requirements no step of the order resolves (design 47 G section 357): the entry is held, naming the candidates, and nothing is guessed", + "items": { + "$ref": "#/defs/Held" + } } }, "required": [ @@ -835,6 +842,25 @@ "catalog" ] }, + "Held": { + "type": "object", + "description": "one requirement an entry is held on, for want of a provider", + "properties": { + "provision": { + "type": "string" + }, + "candidates": { + "type": "array", + "description": "a provisioning edge to each placed provider of it, none chosen; none when no placed module provides it", + "items": { + "$ref": "#/defs/Edge" + } + } + }, + "required": [ + "provision" + ] + }, "NodeEntryRef": { "type": "object", "properties": { diff --git a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts index ee5a58f3..ef4554b2 100644 --- a/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts +++ b/modules/mesh-delivery-contracts/ts/seats/meshwants/index.d.ts @@ -143,6 +143,8 @@ export interface AssignmentEntry { "edges"?: Edge[]; /** the fingerprint it is to run */ "fp": string; + /** the requirements no step of the order resolves (design 47 G section 357): the entry is held, naming the candidates, and nothing is guessed */ + "held"?: Held[]; "module": string; "node": string; /** the layout version */ @@ -250,6 +252,13 @@ export interface Fill { "store": string; } +/** one requirement an entry is held on, for want of a provider */ +export interface Held { + /** a provisioning edge to each placed provider of it, none chosen; none when no placed module provides it */ + "candidates"?: Edge[]; + "provision": string; +} + export interface NodeEntryRef { /** the entry's digest, its object's name */ "digest": string;