From 68b0d0c10a88cff2ab573b164326c4308e8397d5 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 31 Aug 2026 12:54:27 +0200 Subject: [PATCH] What answers a requirement is applied before what asked for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host does not sort, so the order written here is the order a machine applies. Selection walks outward from what was assigned, which puts a consumer before the thing it pulled in — and a service that reads a file another module writes then starts before the file exists. It fails, and the next reconcile fixes it. That is the worst shape a fault can take: what gets remembered is that it works, and nobody looks again. It is 04-ISSUES/013 one level up from where that was found — there, the mesh's own computed files came after a module's resources; here, a whole module comes after the one that needed it. Nothing had hit it because no module until now both required something with resources of its own and had a resource depending on it. Writing the resolver module was what made it reachable, and it would have shown up as dnsmasq failing once on every fresh machine and working ever after. Unrelated modules keep the order selection gave them — assigned first, then what they pulled in. That order is meaningful, and reshuffling it would make every declaration's diff unreadable for no gain. Two modules requiring each other are both applied rather than refused: a cycle is not a machine that cannot work, and refusing would make a cooperating pair impossible to assign. --- internal/catalogue/filtering_test.go | 76 ++++++++++++++++++++++++++++ internal/catalogue/resolve.go | 67 +++++++++++++++++++++++- 2 files changed, 142 insertions(+), 1 deletion(-) diff --git a/internal/catalogue/filtering_test.go b/internal/catalogue/filtering_test.go index 408f376..6a2884e 100644 --- a/internal/catalogue/filtering_test.go +++ b/internal/catalogue/filtering_test.go @@ -502,3 +502,79 @@ func TestAServiceCanReflectAnotherModulesFile(t *testing.T) { } t.Fatal("no service in the declaration") } + +// What answers a requirement is applied before what asked for it. +// +// The host does not sort, so the order written here is the order a machine applies. Selection +// walks outward from what was assigned, which puts a consumer first — and a service that reads a +// file another module writes then starts before the file exists, fails, and is fixed by the next +// reconcile. That is the worst shape a fault can take: what gets remembered is that it works. +func TestWhatAnsweredARequirementIsAppliedFirst(t *testing.T) { + got, err := Resolve(map[string]Manifest{ + "dnsmasq": {Module: "dnsmasq", Requires: []string{"resolver-data"}}, + "data": {Module: "data", Provides: []Offer{{Name: "resolver-data"}}}, + }, []string{"dnsmasq"}, Node{Name: "anchor"}, World{}) + if err != nil { + t.Fatal(err) + } + if got.Modules[0].Module != "data" { + t.Fatalf("the consumer is applied before what it requires: %v", named(got)) + } +} + +// Through a chain, not only one step: a machine applies what the thing it needs needs, first. +func TestAChainOfRequirementsIsAppliedFromTheBottom(t *testing.T) { + got, err := Resolve(map[string]Manifest{ + "top": {Module: "top", Requires: []string{"middle-thing"}}, + "middle": {Module: "middle", Provides: []Offer{{Name: "middle-thing"}}, Requires: []string{"bottom-thing"}}, + "bottom": {Module: "bottom", Provides: []Offer{{Name: "bottom-thing"}}}, + }, []string{"top"}, Node{Name: "anchor"}, World{}) + if err != nil { + t.Fatal(err) + } + order := named(got) + if order[0] != "bottom" || order[1] != "middle" || order[2] != "top" { + t.Fatalf("a chain was not applied from the bottom: %v", order) + } +} + +// Modules with no relation keep the order selection gave them — assigned first, then what they +// pulled in. A sort that reshuffled unrelated modules would make every declaration's diff +// unreadable for no gain. +func TestUnrelatedModulesKeepTheirOrder(t *testing.T) { + got, err := Resolve(map[string]Manifest{ + "one": {Module: "one"}, + "two": {Module: "two"}, + "three": {Module: "three"}, + }, []string{"one", "two", "three"}, Node{Name: "anchor"}, World{}) + if err != nil { + t.Fatal(err) + } + order := named(got) + if order[0] != "one" || order[1] != "two" || order[2] != "three" { + t.Fatalf("unrelated modules were reshuffled: %v", order) + } +} + +// Two modules requiring each other are both applied rather than refused: a cycle here is not a +// machine that cannot work, and refusing would make a cooperating pair impossible to assign. +func TestTwoModulesRequiringEachOtherAreStillApplied(t *testing.T) { + got, err := Resolve(map[string]Manifest{ + "a": {Module: "a", Provides: []Offer{{Name: "from-a"}}, Requires: []string{"from-b"}}, + "b": {Module: "b", Provides: []Offer{{Name: "from-b"}}, Requires: []string{"from-a"}}, + }, []string{"a"}, Node{Name: "anchor"}, World{}) + if err != nil { + t.Fatal(err) + } + if len(got.Modules) != 2 { + t.Fatalf("a cooperating pair was not applied: %v", named(got)) + } +} + +func named(r Resolution) []string { + var out []string + for _, m := range r.Modules { + out = append(out, m.Module) + } + return out +} diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index 4f6e41a..0fc14f5 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -422,7 +422,7 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world } resolution := Resolution{Node: node.Name, At: node.At, Because: because, Needs: needs} - for _, n := range order { + for _, n := range providersFirst(order, catalogue) { resolution.Modules = append(resolution.Modules, catalogue[n]) } @@ -569,3 +569,68 @@ func FromAnywhere(names ...string) []Offer { // ships and this package must not depend on the thing it resolves. The name being wrong would // show up as a refusal naming a module nobody can assign, which a test checks. const meshNetwork = "networking" + +// providersFirst orders a node's modules so that what answers a requirement comes before what +// asked for it. +// +// **The host does not sort** (novox/hq ADR 0005), so the order written here is the order a machine +// applies. Selection walks outward from what was assigned, which puts a consumer before the thing +// it pulled in — and a module whose service reads a file another module writes then starts before +// the file exists. +// +// It fails, and the next reconcile fixes it. That is the worst shape a fault can take: what gets +// remembered is that it works, and nobody looks again +// ([04-ISSUES/013](novox/hq)). This is the same fault one level up from where that was found. +// +// **Stable where nothing requires anything.** Modules with no relation keep the order selection +// gave them, because that order is meaningful — assigned first, then what they pulled in — and a +// sort that reshuffled unrelated modules would make every declaration's diff unreadable. +func providersFirst(order []string, shelf map[string]Manifest) []string { + // Who offers what, among the modules actually chosen. + offeredBy := map[string]string{} + for _, name := range order { + for _, offered := range shelf[name].Offers() { + if _, taken := offeredBy[offered]; !taken { + offeredBy[offered] = name + } + } + } + + needs := map[string][]string{} + for _, name := range order { + for _, want := range shelf[name].Wants() { + provider, known := offeredBy[want] + if !known || provider == name { + continue + } + needs[name] = append(needs[name], provider) + } + } + + var out []string + placed := map[string]bool{} + var place func(string, map[string]bool) + place = func(name string, visiting map[string]bool) { + if placed[name] { + return + } + if visiting[name] { + // Two modules requiring each other. Left in the order selection gave them rather + // than refused: a cycle here is not a machine that cannot work — both are applied, + // and one of them starts before the other is ready and settles on the next pass. + // Refusing would make a pair of modules that cooperate impossible to assign. + return + } + visiting[name] = true + for _, provider := range needs[name] { + place(provider, visiting) + } + delete(visiting, name) + placed[name] = true + out = append(out, name) + } + for _, name := range order { + place(name, map[string]bool{}) + } + return out +}