From 83391dba1b934590e64be81c6ebcccb1623e64f7 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 30 Oct 2026 21:35:52 +0100 Subject: [PATCH] Wait in rule 8 only for the provisions the composition keys (issue 774) Rule 8 asked a key for every `requires`, and the plans key only a handed module's needs and what a helper answers: grafana waited on ace for oidc-client, which the controller still makes, and mesh-build-go for go-toolchain on its own machine, for ever. MissingProvisions now takes catalogue.Awaited, the one rule the plan composition and mesh-provisioner key by, judged against the providers placed somewhere. Quick fix: drop the missing-key check. Thorough fix (chosen): the shared rule in mesh-sdk, so the three parts cannot drift again. Needs mesh-sdk at 30098229b831. --- .../cmd/mesh-delivery-assigner/pass.go | 16 +++++- modules/mesh-delivery-assigner/go.mod | 2 +- modules/mesh-delivery-assigner/go.sum | 2 + .../internal/assign/wants.go | 17 +++--- .../internal/assign/wants_test.go | 54 ++++++++++++++++--- 5 files changed, 77 insertions(+), 14 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 a9f304b..7d7f8c8 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -180,7 +180,7 @@ func (p *passer) wants(ctx context.Context) { for name := range names { fp := trunk[name].FP2 name := name - m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Chosen: chosenOf(chosen, name), Provisions: provisionsOf(name), Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name], + m := assign.Module{Name: name, FP: fp, Manifest: manifests[name], Chosen: chosenOf(chosen, name), Provisions: provisionsOf(name), Answering: answering(manifests, nodes), Nodes: nodesOf[name], Now: time.Now(), Puts: puts[name], Self: func(node string) bool { return placed[node] }, Composed: func(node, fp string) (delivery.ComposedDigest, bool) { if placed[node] && p.w.handedAway() { @@ -1238,3 +1238,17 @@ func (p *passer) seatHolders(ctx context.Context) { p.w.put(delivery.SeatHoldersBucket, key, nil) } } + +// answering is, by provision, the manifests of the modules placed on some node that provide it: what a want's +// provisions are judged by (catalogue.Awaited, issue 774). A provider in the catalogue placed nowhere answers nobody. +func answering(manifests map[string]catalogue.Manifest, n assign.Nodes) func(string) []catalogue.Manifest { + return func(provision string) []catalogue.Manifest { + var out []catalogue.Manifest + for _, name := range slices.Sorted(maps.Keys(n.Providers[provision])) { + if m, ok := manifests[name]; ok && len(n.Providers[provision][name]) > 0 { + out = append(out, m) + } + } + return out + } +} diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index b340ced..7dc5f18 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030203438-30098229b831 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 156a748..3938505 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,7 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2 h1:RF73nVI1dTrrmlFosVDTMJmpUQ+rEeB7++BHH/RvZFg= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030201806-eff81f1220e2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030203438-30098229b831 h1:gpnbyGBmkDsHAlRbZNViDQ/I1y37HSUT7rMProwjEaI= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261030203438-30098229b831/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 237d38a..e7f7d42 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -33,6 +33,9 @@ type Module struct { // Provisions is the seat mesh-provisioner's provisions this module consumes on a node, by key (ADR 0570 §4): the want // waits until each is provided. Nil: the seat's state is not read (not delivered yet), and nothing waits on it. Provisions func(node string) map[string]provisions.Provision + // Answering is the manifests of the modules placed somewhere that provide a provision: what catalogue.Awaited + // judges a want's provisions by (issue 774). Nil: none known, and no key is waited for that is not there. + Answering func(provision string) []catalogue.Manifest // 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 @@ -158,10 +161,13 @@ func (m Module) provisionWaiting(node string) (string, bool) { return "", false } -// MissingProvisions are the provisions the module requires (not optionally) that have no key on node yet, sorted; none -// while the seat's state is not read. +// MissingProvisions are the provisions a want of the module waits for (catalogue.Awaited, the one rule the plan +// composition and the seat mesh-provisioner key by, issue 774) that have no key on node yet, sorted; none while the +// seat's state is not read. A requirement the composition keys nothing for (a toolchain on the module's own machine, a +// login the controller still makes for a module not handed) is not waited for: on 2026-10-30 every new build of +// grafana, dnsmasq, mesh-knowledge, mesh-mail and the builders waited here for a key nobody would make. func (m Module) MissingProvisions(node string) []string { - if m.Provisions == nil { + if m.Provisions == nil || m.Answering == nil { return nil } have := map[string]bool{} @@ -169,12 +175,11 @@ func (m Module) MissingProvisions(node string) []string { have[p.Provision] = true } var out []string - for _, r := range m.Manifest.Requires { - if !have[r] && !m.Manifest.OptionalRequires[r] && !slices.Contains(out, r) { + for _, r := range catalogue.Awaited(m.Manifest, m.Answering) { + if !have[r] { out = append(out, r) } } - slices.Sort(out) return out } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 06886cb..f9326d8 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -104,13 +104,55 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { t.Errorf("provided provisions held g14's want: %+v (%s)", out.Wants["g14"], out.Rules["g14"].Words) } } - // 1954/D1 (the lead, 2026-10-29): a required provision with no key yet holds the want, named. + // 1954/D1 (the lead, 2026-10-29): a required provision with no key yet holds the want, named; and only one the + // composition keys (catalogue.Awaited, issue 774), replaying 2026-10-30: grafana (not handed) waited on ace for + // oidc-client, which the controller still makes, and mesh-build-go (handed) for go-toolchain on its own machine. { - m, n := ready() - m.Provisions = func(string) map[string]provisions.Provision { return map[string]provisions.Provision{} } - if out := Wants(m, n, wantedOld(), nil); out.Rules["g14"].Rule != delivery.RuleProvisions || - !strings.Contains(out.Rules["g14"].Words, "provision database has no key yet") || len(m.MissingProvisions("g14")) != 1 { - t.Errorf("a provision with no key: %+v", out.Rules["g14"]) + handed := &catalogue.Handed{Source: "s", Digest: "d", Templates: map[string]string{"c": "c"}} + keycloak := catalogue.Manifest{Module: "keycloak", Provides: []catalogue.Offer{{Name: "oidc-client", Scope: catalogue.ScopeMesh}}} + proxy := catalogue.Manifest{Module: "route-proxy", Provides: []catalogue.Offer{{Name: "route", Scope: catalogue.ScopeMesh, + AnsweredBy: catalogue.AnsweredByHelper}}} + store := catalogue.Manifest{Module: "store", Provides: []catalogue.Offer{{Name: "database", Scope: catalogue.ScopeMesh}}} + goTools := catalogue.Manifest{Module: "go", Provides: []catalogue.Offer{{Name: "go-toolchain", Scope: catalogue.ScopeNode}}} + gitea := catalogue.Manifest{Module: "gitea", Provides: []catalogue.Offer{{Name: "forge-account", Scope: catalogue.ScopeMesh}}} + answering := func(p string) []catalogue.Manifest { + for _, m := range []catalogue.Manifest{keycloak, proxy, store, goTools, gitea} { + if m.Provides[0].Name == p { + return []catalogue.Manifest{m} + } + } + return nil + } + for _, c := range []struct { + name string + manifest catalogue.Manifest + missing string // the provision named, "" for none: the want moves + }{ + {"a handed module's database with no key", catalogue.Manifest{Module: "mail", Moved: true, Handed: handed, + Requires: []string{"database"}}, "database"}, + {"a module not handed waits for no login the controller makes (grafana on ace, oidc-client)", + catalogue.Manifest{Module: "mail", Requires: []string{"oidc-client"}}, ""}, + {"a module not handed waits for its route, which the helper answers (grafana's route)", + catalogue.Manifest{Module: "mail", Requires: []string{"oidc-client", "route"}}, "route"}, + {"a handed builder waits for no toolchain on its own machine (mesh-build-go, go-toolchain)", + catalogue.Manifest{Module: "mail", Moved: true, Handed: handed, Requires: []string{"go-toolchain"}}, ""}, + {"and does wait for its forge account", catalogue.Manifest{Module: "mail", Moved: true, Handed: handed, + Requires: []string{"go-toolchain", "forge-account"}}, "forge-account"}, + } { + m, n := ready() + m.Manifest, m.Answering = c.manifest, answering + m.Provisions = func(string) map[string]provisions.Provision { return map[string]provisions.Provision{} } + out := Wants(m, n, wantedOld(), nil) + if c.missing == "" { + if out.Wants["g14"].FP != fNew || len(m.MissingProvisions("g14")) != 0 { + t.Errorf("%s: held %+v", c.name, out.Rules["g14"]) + } + continue + } + if out.Rules["g14"].Rule != delivery.RuleProvisions || !strings.Contains(out.Rules["g14"].Words, + "provision "+c.missing+" has no key yet") || len(m.MissingProvisions("g14")) != 1 { + t.Errorf("%s: %+v", c.name, out.Rules["g14"]) + } } } // 1960: rule 5 holds no want, and no module is left without a first want for a provider down or a node offline; the