diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go index cadba393..32069148 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/integration_test.go @@ -446,6 +446,16 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { dbDigest := b.manifest(t, "db", `{"module":"db","version":"1",`+offer+`}`) appDigest := b.manifest(t, "app", `{"module":"app","version":"1","requires":["database"]}`) toolDigest := b.manifest(t, "tool", `{"module":"tool","version":"1"}`) + asked := func(state, why string) { + raw, err := provisions.Encode(provisions.Provision{V: provisions.Version, Provision: "database", Consumer: provisions.End{Module: "app", Node: "ace"}, + Provider: provisions.End{Module: "db", Node: "ace"}, State: state, Why: why, At: time.Now()}) + if err != nil { + t.Fatal(err) + } + b.putJSON(t, provisions.ProvisionsBucket, "ace.app.database", raw) + } + // The provisioner keys it from the manifests, as soon as they are known and whether the provider is placed yet or not. + asked(provisions.Asked, "the provider's build is not done") fpOf := func(c byte) string { return delivery.SourcePrefix2 + strings.Repeat(string(c), 64) } toolOld := fpOf('1') for module, row := range map[string]struct { @@ -469,15 +479,6 @@ func TestWantsAreWrittenAndNoDifferenceIs(t *testing.T) { } runsOld, _ := delivery.EncodeHave(delivery.Have{FP: toolOld, Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) b.putJSON(t, delivery.HaveBucket, "ace.tool", runsOld) - asked := func(state, why string) { - raw, err := provisions.Encode(provisions.Provision{V: provisions.Version, Provision: "database", Consumer: provisions.End{Module: "app", Node: "ace"}, - Provider: provisions.End{Module: "db", Node: "ace"}, State: state, Why: why, At: time.Now()}) - if err != nil { - t.Fatal(err) - } - b.putJSON(t, provisions.ProvisionsBucket, "ace.app.database", raw) - } - asked(provisions.Asked, "the provider's build is not done") if w := b.wantNow(t, "ace", "tool", "the build module's want at the second build", func(w delivery.Want) bool { return w.FP == fpOf('4') }); w.FP != fpOf('4') { t.Errorf("tool's want = %+v", w) } 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 1992c412..73e1b9c4 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass.go @@ -158,7 +158,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), Answering: answering(manifests, nodes), 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), Offered: offered(manifests), 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) { return p.w.composed(node, name, fp) // a placed node's own: the digest its own have names (ADR 0571 §6) @@ -1086,6 +1086,26 @@ func (p *passer) seatHolders(ctx context.Context) { } } +// offered says whether some manifest of the trunk, or a module the mesh provides itself, offers a provision under its name +// (a module offers its own name too, catalogue.Manifest.Offers): what a requirement nobody offers is told from. +func offered(manifests map[string]catalogue.Manifest) func(string) bool { + names := map[string]bool{} + add := func(m catalogue.Manifest) { + for _, o := range m.Offers() { + names[o] = true + } + } + for _, m := range manifests { + add(m) + } + for _, module := range delivery.ProvidedModules() { + if m, _, err := delivery.ProvidedManifest(module); err == nil { + add(m) + } + } + return func(provision string) bool { return names[provision] } +} + // 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 { diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index d9912d36..79baa718 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -111,6 +111,14 @@ func TestAHoldAskedAgainAfterALiftAndARestartHasANewID(t *testing.T) { // 1906/H4: an answer counts only for the asking it answered: an answer to an earlier hold of the same fingerprint, or to an ask // taken away, is not taken for the hold that stands now. func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) { + // ADR 0603 §5: a provision is offered by a manifest's provides, by a module's own name, and by a module the mesh provides + // itself (mesh-addressing, mesh-wireguard's); nothing else. + off := offered(map[string]catalogue.Manifest{"node-tools": {Module: "node-tools"}, "db": {Module: "db", Provides: []catalogue.Offer{{Name: "database"}}}}) + for provision, want := range map[string]bool{"node-tools": true, "database": true, "mesh-addressing": true, "queue": false} { + if off(provision) != want { + t.Errorf("offered(%s) = %v, want %v", provision, !want, want) + } + } key, _ := delivery.HoldAskKey("web", fpTest) p := newPasser(buckets{}, time.Now()) put := func(asking int) string { diff --git a/modules/mesh-delivery-assigner/internal/assign/wants.go b/modules/mesh-delivery-assigner/internal/assign/wants.go index 56704d68..ac7cc4fa 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants.go @@ -36,6 +36,10 @@ type Module struct { // 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 + // Offered says whether any module offers a provision by name or by its own name (catalogue.Manifest.Offers) among every + // manifest the trunk holds and the modules the mesh provides itself, placed or not. Nil: not known, and no requirement + // is waited on for want of an offer. + Offered 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 @@ -154,10 +158,12 @@ func (m Module) provisionWaiting(node string) (string, bool) { } keyed := m.Provisions(node) // Only a provision the newest manifest still requires is waited on (ADR 0603 §9): a key of an older revision's edge, - // asked or retiring for the operator's answer (ADR 0570 §5), holds nothing. A requirement catalogue.Awaited drops for a - // reason the decision keeps (optional, not keyed: a toolchain on the module's own machine, a login the controller makes) is - // not waited on; one it drops only because no answering provider is known yet is (ADR 0603 §5: an unknown provider has - // provided nothing), so a consumer never moves ahead of a provider whose module the indexer has not yet taken in. + // asked or retiring for the operator's answer (ADR 0570 §5), holds nothing. A requirement catalogue.Awaited leaves out is + // not waited on when something offers it (a module's own name, a module the mesh provides itself, a provision the + // composition does not key: optional, a toolchain on the module's own machine, a login the controller makes). One that + // nothing offers is waited on (ADR 0603 §5: an unknown provider has provided nothing), so a consumer never moves ahead of + // a provider whose module the indexer has not yet taken in; it holds loudly, since the watchman raises the hold past its + // bound (ADR 0603 §8). needed := m.Manifest.Requires if m.Answering != nil { needed = catalogue.Awaited(m.Manifest, m.Answering) @@ -166,12 +172,16 @@ func (m Module) provisionWaiting(node string) (string, bool) { have[p.Provision] = true } for _, r := range slices.Sorted(slices.Values(m.Manifest.Requires)) { - if m.Manifest.OptionalRequires[r] || len(m.Answering(r)) > 0 || slices.Contains(needed, r) { + if have[r] && !m.Manifest.OptionalRequires[r] && !slices.Contains(needed, r) { + needed = append(needed, r) // the provisioner keys it from the manifests, whoever is placed yet + continue + } + if m.Offered == nil || m.Manifest.OptionalRequires[r] || len(m.Answering(r)) > 0 || m.Offered(r) || slices.Contains(needed, r) { continue } needed = append(needed, r) if !have[r] { - return fmt.Sprintf("no provider of %s is known yet for %s on %s: an unknown provider has provided nothing", r, m.Name, node), true + return fmt.Sprintf("no module offers %s yet for %s on %s: an unknown provider has provided nothing, so the want waits for one, and the watchman raises it past its bound", r, m.Name, node), true } } } diff --git a/modules/mesh-delivery-assigner/internal/assign/wants_test.go b/modules/mesh-delivery-assigner/internal/assign/wants_test.go index 00c91352..39972c46 100644 --- a/modules/mesh-delivery-assigner/internal/assign/wants_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/wants_test.go @@ -176,6 +176,24 @@ func TestEachRuleHoldsTheWantBack(t *testing.T) { } } } + // ADR 0603 §5: a requirement nobody offers is an unknown provider and waits, loudly; one something offers (a module's own + // name, a module the mesh provides itself) is left to catalogue.Awaited as before and does not. + for _, c := range []struct { + name, requires string + offered bool + }{{"a module's own name (node-tools)", "node-tools", true}, {"a module the mesh provides itself (mesh-addressing)", "mesh-addressing", true}, + {"a provision nobody offers", "database", false}} { + m, n := ready() + m.Manifest.Requires = []string{c.requires} + m.Answering = func(string) []catalogue.Manifest { return nil } + m.Offered = func(p string) bool { return c.offered && p == c.requires } + m.Provisions = func(string) map[string]provisions.Provision { return map[string]provisions.Provision{} } + out := Wants(m, n, wantedOld(), nil) + if got := out.Wants["g14"].FP == fNew; got != c.offered || (!c.offered && (out.Rules["g14"].Rule != delivery.RuleProvisions || + !strings.Contains(out.Rules["g14"].Words, "no module offers database yet") || !strings.Contains(out.Rules["g14"].Words, "past its bound"))) { + t.Errorf("%s: want %+v, held by %+v", c.name, out.Wants["g14"], 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 // provider down is said (Unreached). novox on 2026-10-29: route-proxy down held de-spiegel and link2pay, minio not // healthy held photos, and novox offline left a module with no canary.