diff --git a/go/delivery/placement.go b/go/delivery/placement.go index 60f6e32..d71a3f3 100644 --- a/go/delivery/placement.go +++ b/go/delivery/placement.go @@ -364,12 +364,20 @@ func (p Placement) Held() []catalogue.Held { return out } +// KeptApart are the modules never placed on one node, each row naming the record that asks it (novox/hq ADR 0564 clause 6 +// and Limits 5; issue 1952 S8): no manifest word says it, so the check below holds the table. mesh-watcher hears the +// self-check's `checked` from a second machine (ADR 0227 §6), which on the self-check's own node would fall silent with +// it. +var KeptApart = []struct{ A, B, Record string }{ + {A: "mesh-watcher", B: "mesh-self-check", Record: "ADR 0227 §6, ADR 0564 clause 6"}, +} + // CheckPlacements is the holder's check of a proposal laid over every placement (ADR 0567 §2), over the placements by // node, the modules the indexer holds and the nodes with a record. It returns every refusal, in node order, never only the first: a // placement naming a node not enrolled or a module the catalogue does not hold; two modules of one node on the same host // port; a pin, binding or holding naming a provider that is not placed where it says; a seat of scope mesh held twice, unless the seat is replicated (catalogue.Seat's Replicated, ADR 0223), which // is held once per node; -// two nodes with one address on the private network; more than one hub. +// two nodes with one address on the private network; more than one hub; two modules KeptApart on one node. // A host port a manifest declares without a number and a requirement left unbound are the composition's to find (its // ForeseenPorts and Unbound), which the catalogue's composition check reads. func CheckPlacements(placements map[string]Placement, held func(module string) bool, enrolled func(node string) bool) []string { @@ -411,6 +419,15 @@ func CheckPlacements(placements map[string]Placement, held func(module string) b if !enrolled(node) { refusals = append(refusals, fmt.Sprintf("%s is a node with no record", node)) } + placedHere := map[string]bool{} + for _, m := range p.Modules { + placedHere[m.Module] = true + } + for _, apart := range KeptApart { + if placedHere[apart.A] && placedHere[apart.B] { + refusals = append(refusals, fmt.Sprintf("%s: %s and %s are never placed on one node (%s)", node, apart.A, apart.B, apart.Record)) + } + } hostPorts := map[int]string{} for _, m := range p.Modules { if !held(m.Module) && !ProvidedByMesh(m.Module) { // the mesh provides the private network itself diff --git a/go/delivery/placement_test.go b/go/delivery/placement_test.go index e861328..6ea2dde 100644 --- a/go/delivery/placement_test.go +++ b/go/delivery/placement_test.go @@ -179,6 +179,8 @@ func TestPlacement(t *testing.T) { clash.Modules = append([]PlacedModule{{Module: "ghost", Ports: map[string]int{"80": 18080}}}, g14.Modules...) clash.Modules[2] = PlacedModule{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "ace", Module: "postgres"}}} clash.Pins = map[string]Provider{"shell": {Node: "g14", Module: "bash"}} + // ADR 0564 clause 6, Limits 5 (issue 1952 S8): mesh-watcher and mesh-self-check are never placed on one node. + clash.Modules = append(clash.Modules, PlacedModule{Module: "mesh-watcher"}, PlacedModule{Module: "mesh-self-check"}) twice := novox twice.Node = "stranger" twice.Decided.Address = "10.77.0.2" @@ -186,7 +188,8 @@ func TestPlacement(t *testing.T) { for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080", "bound to ace/postgres", "pin of shell names g14/bash", "stranger is a node with no record", "mesh-store, held once for the mesh, is held on novox and stranger", - "g14 and stranger both have the address 10.77.0.2", "novox, stranger are each placed as it"} { + "g14 and stranger both have the address 10.77.0.2", "novox, stranger are each placed as it", + "g14: mesh-watcher and mesh-self-check are never placed on one node (ADR 0227 §6, ADR 0564 clause 6)"} { found := false for _, line := range r { found = found || strings.Contains(line, want)