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 +}