A want asking bus state waits until a build offering bus-state runs healthy on a node present (ADR 0614 §5; issue 774)

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
This commit is contained in:
2026-11-03 23:28:41 +01:00
parent 702b19fdb1
commit 46c735473f
7 changed files with 92 additions and 11 deletions
@@ -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)
@@ -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
}
@@ -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
}
@@ -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, ""
}
@@ -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"])
}
}
+3 -2
View File
@@ -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)
+14 -3
View File
@@ -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 <verb>` for a verb wait, `reader capability <capabilities>` 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