Three gatherers walk every node and pass over one whose plan will not compose, so that one broken set does not cost the rest. They read a plain error to mean that, and so read a store that was briefly unreachable as a machine running nothing. On the roster of routed names that is not a degraded answer but a false one: it states to every machine at once that another machine's names do not exist. Because the roster is part of every container's identity, a control node replaced every container it ran — its own store, the registry, the edge, mail, the bus — on a six-minute cycle for hours. The loop closed through the store this is read from: each pass restarted it, the read failed, one name left the roster, and the roster changing is every container changing. planFor now marks the two failures that really are the node's own — its set not composing, and a setting that reaches nothing — and the three gatherers pass over those and only those. Every other failure is raised, naming the machine and the read, because a mesh-wide refusal with nothing named in it is the other way to lose an evening. novox/hq 04-ISSUES/152, and 151 for why a changed roster is a changed container.
100 lines
3.2 KiB
Go
100 lines
3.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A node's own set failing to compose, and the mesh being unable to answer at all, are different
|
|
// things, and only the first may be passed over when something is gathered across every machine
|
|
// (novox/hq 04-ISSUES/152). These pin that distinction where the three gatherers rely on it.
|
|
|
|
func TestASetThatDoesNotComposeIsMarkedAsTheNodesOwnProblem(t *testing.T) {
|
|
open := aMesh(t)
|
|
one, two := rivals()
|
|
register(t, open, one)
|
|
register(t, open, two)
|
|
for _, m := range []string{one.Module, two.Module} {
|
|
if _, err := open.inventory.Assign(t.Context(), "laptop", m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
_, _, err := planFor(t.Context(), open, "laptop")
|
|
if err == nil {
|
|
t.Fatal("two modules claiming one seat composed anyway")
|
|
}
|
|
if !unresolvable(err) {
|
|
t.Fatalf("a set that cannot compose was not marked as the node's own problem: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAStoreThatCannotBeReadIsNotANodeThatDoesNotCompose(t *testing.T) {
|
|
open := aMesh(t)
|
|
|
|
// Nothing is wrong with anchor. The question simply cannot be asked.
|
|
stopped, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
|
|
_, _, err := planFor(stopped, open, "anchor")
|
|
if err == nil {
|
|
t.Fatal("a plan composed against a store that could not be read")
|
|
}
|
|
if unresolvable(err) {
|
|
t.Fatalf("a question the mesh could not answer was read as a node that runs nothing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOneIncoherentNodeDoesNotCostTheRestTheirNames(t *testing.T) {
|
|
open := aMesh(t)
|
|
one, two := rivals()
|
|
register(t, open, one)
|
|
register(t, open, two)
|
|
for _, m := range []string{one.Module, two.Module} {
|
|
if _, err := open.inventory.Assign(t.Context(), "laptop", m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// laptop cannot compose. That is laptop's problem and nobody else's: the roster is still
|
|
// answerable, and anchor keeps whatever it serves.
|
|
if _, err := routeNamesInTheMesh(t.Context(), open); err != nil {
|
|
t.Fatalf("one node's broken set cost the whole mesh its roster: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestARosterIsNeverReturnedWithNamesItCouldNotRead(t *testing.T) {
|
|
open := aMesh(t)
|
|
|
|
stopped, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
|
|
names, err := routeNamesInTheMesh(stopped, open)
|
|
if err == nil {
|
|
t.Fatalf("a roster was composed from a store that could not be read: %v", names)
|
|
}
|
|
// The failure must be raised, not turned into an absence. A roster missing a machine's names
|
|
// is indistinguishable, on every machine that receives it, from the operator withdrawing them —
|
|
// and because the roster is part of every container's identity, it replaces all of them.
|
|
if names != nil {
|
|
t.Fatalf("a partial roster was returned beside the error: %v", names)
|
|
}
|
|
}
|
|
|
|
// Kept so the reason survives the next person reading it: the message the gatherer raises must say
|
|
// which machine could not be read, or the operator is left with a mesh-wide failure and no name.
|
|
func TestTheRaisedFailureNamesTheMachineItCouldNotRead(t *testing.T) {
|
|
open := aMesh(t)
|
|
stopped, cancel := context.WithCancel(t.Context())
|
|
cancel()
|
|
|
|
_, err := routeNamesInTheMesh(stopped, open)
|
|
if err == nil {
|
|
t.Fatal("no failure was raised")
|
|
}
|
|
if !strings.Contains(err.Error(), "cannot be read") {
|
|
t.Fatalf("the failure does not say the mesh could not be read: %v", err)
|
|
}
|
|
}
|