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
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.
This commit is contained in:
2026-08-31 12:54:27 +02:00
parent bff893f8af
commit 68b0d0c10a
2 changed files with 142 additions and 1 deletions
+76
View File
@@ -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
}
+66 -1
View File
@@ -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
}