From 46c735473f6f9be73c8a18565ca5bf0c624df242 Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 3 Nov 2026 23:28:41 +0100 Subject: [PATCH] =?UTF-8?q?A=20want=20asking=20bus=20state=20waits=20until?= =?UTF-8?q?=20a=20build=20offering=20bus-state=20runs=20healthy=20on=20a?= =?UTF-8?q?=20node=20present=20(ADR=200614=20=C2=A75;=20issue=20774)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bus-state is answered with no credential of a consumer's own, so ADR 0603 §5 counts the provider's own have: the assigner reads, per pass, what the running healthy builds on online nodes offer (their manifests through the builder's builds) and holds a consumer with a bus-state entry under the provisions' rule until one offers it, handed or not and with no provisioner key. The words name bus state, and the delivery view names the edge from them. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- .../cmd/mesh-delivery-assigner/pass.go | 2 + .../cmd/mesh-delivery-assigner/verbs.go | 47 +++++++++++++++++-- .../internal/assign/verbs.go | 12 +++++ .../internal/assign/wants.go | 7 +++ .../internal/assign/wants_test.go | 13 ++++- sdk/graph/graph_test.go | 5 +- sdk/graph/waits.go | 17 +++++-- 7 files changed, 92 insertions(+), 11 deletions(-) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go index 6170b6a2..cc520b40 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -158,6 +158,7 @@ func (p *passer) wants(ctx context.Context) { // The verb edges, once for the pass (ADR 0614 §2, §3). moduleGraph := p.moduleGraph() served := p.served(ctx, get, moduleGraph, nodes) + offering := p.offering(ctx, get, manifests, nodes) // bus state (ADR 0614 §5) for name := range names { fp := trunk[name].FP2 name := name @@ -198,6 +199,7 @@ func (p *passer) wants(ctx context.Context) { if served != nil { m.Invokes, m.Served = keptInvokes(moduleGraph, name), served } + m.Offering = offering last := map[string]string{} for _, node := range m.Nodes { last[node] = p.w.lastHealthy(node, name, fp) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/verbs.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/verbs.go index 049a42e3..28fc87b9 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/verbs.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/verbs.go @@ -4,6 +4,7 @@ import ( "context" "slices" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/graph" @@ -69,23 +70,59 @@ func (p *passer) served(ctx context.Context, get getter, g graph.Graph, n assign return served } +// offering is, for this pass, whether a build whose manifest offers a provision runs healthy on a node present (ADR 0603 +// §5, ADR 0614 §5): every running healthy have on an online node, its build's manifest read as servesOf reads it. A +// healthy build whose manifest cannot be read counts what its module's trunk manifest offers, so a build read late never +// holds a consumer for good. +func (p *passer) offering(ctx context.Context, get getter, trunk map[string]catalogue.Manifest, n assign.Nodes) func(string) bool { + offered := map[string]bool{} + for node, haves := range n.Haves { + if !n.Online[node] { + continue + } + for module, h := range haves { + if h.Phase != delivery.PhaseRunning || h.Health != delivery.HealthOK { + continue + } + m, ok := p.manifestOf(ctx, get, module, h.FP) + if !ok { + m = trunk[module] + } + for _, o := range m.Offers() { + offered[o] = true + } + } + } + return func(provision string) bool { return offered[provision] } +} + // servesOf are the verbs the manifest of module's build at fp serves (graph.ServesOf), and whether it could be read. func (p *passer) servesOf(ctx context.Context, get getter, module, fp string) ([]string, bool) { + m, ok := p.manifestOf(ctx, get, module, fp) + if !ok { + return nil, false + } + return graph.ServesOf(m), true +} + +// manifestOf is the manifest of module's build at fp (the builder's build names its digest in the indexer's manifests), +// and whether it could be read. +func (p *passer) manifestOf(ctx context.Context, get getter, module, fp string) (catalogue.Manifest, bool) { key, err := delivery.BuilderBuildKey(module, fp) if err != nil { - return nil, false + return catalogue.Manifest{}, false } raw := p.w.value(delivery.BuilderBuildsBucket, key) if raw == nil { - return nil, false + return catalogue.Manifest{}, false } b, err := delivery.DecodeBuilderBuild(raw) if err != nil || b.Manifest == "" { - return nil, false + return catalogue.Manifest{}, false } got := p.w.manifest(ctx, get, b.Manifest) if got.err != "" { - return nil, false + return catalogue.Manifest{}, false } - return graph.ServesOf(got.manifest), true + return got.manifest, true } diff --git a/modules/mesh-delivery-assigner/internal/assign/verbs.go b/modules/mesh-delivery-assigner/internal/assign/verbs.go index 79714dcc..f41d4360 100644 --- a/modules/mesh-delivery-assigner/internal/assign/verbs.go +++ b/modules/mesh-delivery-assigner/internal/assign/verbs.go @@ -22,3 +22,15 @@ func (m Module) verbWaiting(node string) (string, bool) { } return "", false } + +// busStateWaiting says why the module's want on node waits for bus state (ADR 0614 §5): its manifest asks bus state (a +// bus-state entry, as the module graph reads it) and no build whose manifest offers bus-state runs healthy on a node +// present. The provision bus-state is answered with no credential of a consumer's own, so ADR 0603 §5 counts the +// provider's own have and no provisioner key is waited for, handed or not. Nil Offering (nothing read of what runs) waits +// for none. It holds under the provisions' rule, its words naming bus state (graph.BusStateWaitWords). +func (m Module) busStateWaiting(node string) (string, bool) { + if len(m.Manifest.BusState) == 0 || m.Offering == nil || m.Offering(graph.BusState) { + return "", false + } + return graph.BusStateWaitWords(m.Name, node), true +} diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index f75faf8f..5057439e 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -49,6 +49,10 @@ type Module struct { // Served says whether a build serving a seat verb or module tool runs healthy, counted from its holder's have (ADR 0614 // §2, as ADR 0603 §5 counts a provider's). Nil: nothing is read of what runs, and no want waits on a verb. Served func(invoke string) bool + // Offering says whether a build whose manifest offers a provision runs healthy on a node present, counted from the + // provider's own have (ADR 0603 §5): what a want asking bus state waits for (ADR 0614 §5). Nil: nothing is read of what + // runs, and no want waits on bus state. + Offering func(provision string) bool // Chosen is a person's choice of the module's upgrade policy (the assigner's `policies`, ADR 0565 §7); nil: none. Chosen *catalogue.PersonPolicy Nodes []string @@ -165,6 +169,9 @@ func holds(m Module, node string, n Nodes, ordering bool) (bool, int, string) { if words, waiting := m.verbWaiting(node); waiting { return false, delivery.RuleProvisions, words } + if words, waiting := m.busStateWaiting(node); waiting { + return false, delivery.RuleProvisions, words + } return true, 0, "" } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index dceaec80..96f86432 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -158,12 +158,23 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { {"once a build serving it runs", []string{"seat:mesh-catalog.modules"}, []string{"seat:mesh-catalog.modules"}, false, ""}, {"an edge left out on a cycle", nil, nil, false, ""}, {"nothing read of what runs", []string{"seat:mesh-catalog.modules"}, nil, true, ""}, + // ADR 0614 §5: a module asking bus state is held while no build offering bus-state runs, and moves once one does. + {"bus state no running build offers", nil, nil, false, graph.BusState}, + {"bus state a running build offers", nil, []string{graph.BusState}, false, ""}, } { m, n := ready() m.Invokes = c.invokes if !c.nilRead { m.Served = func(v string) bool { return slices.Contains(c.served, v) } } + if strings.HasPrefix(c.name, "bus state") { + m.Manifest.BusState = []catalogue.BusStateEntry{{Name: "cache", Storage: "key-value"}} + m.Offering = func(p string) bool { return slices.Contains(c.served, p) } + } + words := graph.VerbWaitWords(c.holds, "mail", "g14") + if c.holds == graph.BusState { + words = graph.BusStateWaitWords("mail", "g14") + } out := Wants(m, n, wantedOld(), nil) if c.holds == "" { if out.Wants["g14"].FP != fNew { @@ -171,7 +182,7 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { } continue } - if out.Wants["g14"].FP == fNew || out.Rules["g14"].Rule != delivery.RuleProvisions || out.Rules["g14"].Words != graph.VerbWaitWords(c.holds, "mail", "g14") { + if out.Wants["g14"].FP == fNew || out.Rules["g14"].Rule != delivery.RuleProvisions || out.Rules["g14"].Words != words { t.Errorf("%s: want %+v, held by %+v", c.name, out.Wants["g14"], out.Rules["g14"]) } } diff --git a/sdk/graph/graph_test.go b/sdk/graph/graph_test.go index ad97e5a3..e5c7fdd0 100644 --- a/sdk/graph/graph_test.go +++ b/sdk/graph/graph_test.go @@ -135,8 +135,9 @@ func TestGraphEdges(t *testing.T) { for words, want := range map[string]string{ VerbWaitWords("seat:mesh-catalog.modules", "artifact-store-tools", "g14"): "verb seat:mesh-catalog.modules", ReaderWaitWords("g14", []string{"tool-runner", "bus-state"}, "node-tools", "src2:abcdef"): "reader capability tool-runner, bus-state", - "the verb is served": "", - "the node-engine of g14 has not started": "", + BusStateWaitWords("keeper", "g14"): "bus state", + "the verb is served": "", + "the node-engine of g14 has not started": "", } { if got := EdgeOfWords(words); got != want { t.Errorf("the edge of %q = %q, want %q", words, got, want) diff --git a/sdk/graph/waits.go b/sdk/graph/waits.go index eea7dcb2..3cbfd5ce 100644 --- a/sdk/graph/waits.go +++ b/sdk/graph/waits.go @@ -5,9 +5,9 @@ import ( "strings" ) -// The words of a want held on an edge of ADR 0614 (novox/hq ADR 0614 §2, §4): no reader knows a rule number of its own for +// The words of a want held on an edge of ADR 0614 (novox/hq ADR 0614 §2, §4, §5): no reader knows a rule number of its own for // either yet, so the assigner holds them under the provisions' and the composition's rules, and these words are the one -// place they are written and read. The assigner writes them (VerbWaitWords, ReaderWaitWords); the delivery view names +// place they are written and read. The assigner writes them (VerbWaitWords, ReaderWaitWords, BusStateWaitWords); the delivery view names // the edge a want waits on from them (EdgeOfWords). const ( @@ -16,8 +16,16 @@ const ( readerOpen = "the node-engine of " readerMid = " does not read " readerTail = ", which the manifest of " + busOpen = "bus state: " ) +// BusStateWaitWords are the words of a want of module on node held until a build of a module offering bus-state runs +// healthy on a node present (ADR 0614 §5, through ADR 0603 §5's count of the provider's own have). +func BusStateWaitWords(module, node string) string { + return fmt.Sprintf("%s%s asks bus state, and no build of a module offering %s runs healthy on a node present: its want on %s waits for one (ADR 0614 §5)", + busOpen, module, BusState, node) +} + // VerbWaitWords are the words of a want of module on node held until a build serving verb runs healthy (ADR 0614 §2). func VerbWaitWords(verb, module, node string) string { return fmt.Sprintf("%s%s %s%s: its want on %s waits until a build serving it runs healthy (ADR 0614 §2)", verbOpen, verb, @@ -32,8 +40,11 @@ func ReaderWaitWords(node string, missing []string, module, fp string) string { } // EdgeOfWords is the edge a held want's words name: `verb ` for a verb wait, `reader capability ` for -// a reader capability's hold, or "" for words of neither. +// a reader capability's hold, `bus state` for a bus-state wait, or "" for words of none of them. func EdgeOfWords(words string) string { + if strings.HasPrefix(words, busOpen) { + return "bus state" + } if rest, ok := strings.CutPrefix(words, verbOpen); ok { if verb, _, ok := strings.Cut(rest, " "); ok && strings.Contains(rest, verbClose) { return "verb " + verb