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