Three faults, one file split. All from reading, all verified to bite. Unassign now releases the module's ports. ReleasePorts existed, said "for when it is unassigned" in its own comment, and was called by nothing — so a fixed port stayed claimed in the name of a module that was gone, and the next module needing it was refused by a ghost. Kept-once-chosen is a promise about a module that is still here. MachineSide reads addressed mappings. "127.0.0.1:8080:80" was split at the first colon, "127.0.0.1" failed to parse as a port, and the mapping was silently skipped — putting the filter back on the declared port, the exact fault the function was written to end. The machine side is the second-from-last part, which is the reading the host already applies, and the substrate bundle writes that shape today. An allocation race answers in the mesh's words. Two concurrent picks of the same port used to surface as a Postgres constraint violation, verbatim. The table has two keys, so the collision is one of two facts: the racer was this same assignment — then its answer is the answer, kept-once-chosen does not care who chose — or another module took the machine port, and an unfixed pick is simply made again against the moved free list. A fixed port that lost the race is refused by name. Told apart by re-reading the row, not by the constraint's name, so this does not couple to the migration's spelling. And the artifact-store cycle tests moved to bootstrap_cycle_test.go; machineside_test.go had quietly become three subjects.
239 lines
7.6 KiB
Go
239 lines
7.6 KiB
Go
package inventory
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-control/internal/catalogue"
|
|
)
|
|
|
|
func aNodeWithModules(t *testing.T, modules ...string) (*Inventory, string) {
|
|
t.Helper()
|
|
inv := fresh(t)
|
|
ctx := t.Context()
|
|
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, m := range modules {
|
|
if err := inv.RegisterModule(ctx,
|
|
catalogue.Manifest{Module: m, Version: "1"}, Source{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return inv, "anchor"
|
|
}
|
|
|
|
// **Made once and kept.** A port that moved on every declaration would restart both ends each
|
|
// time, and would hand a consumer a number that was true when it was read.
|
|
func TestAPortIsAssignedOnceAndKept(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "postgres")
|
|
first, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Machine != second.Machine {
|
|
t.Fatalf("asking twice moved the port: %d then %d", first.Machine, second.Machine)
|
|
}
|
|
if first.Machine == 5432 {
|
|
t.Error("the mesh handed back the port the module asked for, which is what it cannot know is free")
|
|
}
|
|
}
|
|
|
|
// The fault this exists for: two modules wanting one number, which neither of them chose badly.
|
|
func TestTwoModulesWantingOnePortGetTwo(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "postgres", "another-database")
|
|
a, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := inv.PortFor(t.Context(), node, "another-database", 5432, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Machine == b.Machine {
|
|
t.Fatalf("both were put on %d, which is the collision this exists to prevent", a.Machine)
|
|
}
|
|
}
|
|
|
|
// A port the protocol fixes is used as written, because a mail system elsewhere is not a mail
|
|
// system.
|
|
func TestAFixedPortIsTheOneTheProtocolSays(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "mailu")
|
|
got, err := inv.PortFor(t.Context(), node, "mailu", 25, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Machine != 25 {
|
|
t.Fatalf("mail was put on %d", got.Machine)
|
|
}
|
|
if !got.Fixed {
|
|
t.Error("it does not record that the protocol chose it, so nothing can refuse a second holder")
|
|
}
|
|
}
|
|
|
|
// **A fixed port is a claim**: one holder per machine, refused by name at assignment rather than
|
|
// by a container runtime at apply.
|
|
func TestASecondModuleCannotHaveAFixedPort(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "mailu", "other-mail")
|
|
if _, err := inv.PortFor(t.Context(), node, "mailu", 25, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := inv.PortFor(t.Context(), node, "other-mail", 25, true)
|
|
if err == nil {
|
|
t.Fatal("two modules were given port 25 on one machine")
|
|
}
|
|
if !errors.Is(err, ErrPortTaken) {
|
|
t.Errorf("the refusal is not the one a caller can recognise: %v", err)
|
|
}
|
|
if !contains(err.Error(), "mailu") {
|
|
t.Errorf("the refusal does not say who has it: %v", err)
|
|
}
|
|
}
|
|
|
|
// An assigned port must not land on one the protocol fixed for something else.
|
|
func TestAnAssignedPortAvoidsAFixedOne(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "mailu", "web")
|
|
fixed, err := inv.PortFor(t.Context(), node, "mailu", 20000, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assigned, err := inv.PortFor(t.Context(), node, "web", 8080, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if assigned.Machine == fixed.Machine {
|
|
t.Fatalf("an assignment landed on %d, which the protocol had fixed for something else",
|
|
fixed.Machine)
|
|
}
|
|
}
|
|
|
|
// What a module gave back is available again. Otherwise a machine that ran a hundred modules over
|
|
// a year has a hundred ports it cannot explain.
|
|
func TestUnassigningGivesThePortBack(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "postgres")
|
|
first, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.ReleasePorts(t.Context(), node, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
held, err := inv.PortsFor(t.Context(), node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(held) != 0 {
|
|
t.Fatalf("it still holds %v", held)
|
|
}
|
|
again, err := inv.PortFor(t.Context(), node, "postgres", 5432, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again.Machine != first.Machine {
|
|
t.Errorf("the freed port was not the first one offered again: %d then %d",
|
|
first.Machine, again.Machine)
|
|
}
|
|
}
|
|
|
|
func contains(s, what string) bool {
|
|
return len(s) >= len(what) && (func() bool {
|
|
for i := 0; i+len(what) <= len(s); i++ {
|
|
if s[i:i+len(what)] == what {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})()
|
|
}
|
|
|
|
// **The bug this whole thing is for** (novox/hq 04-ISSUES/028). The mesh keeps its own store on a
|
|
// machine, from the bundle, before there is any mesh to ask. A database module assigned there was
|
|
// handed 5432 — the port the store already had — and found out from a container runtime.
|
|
func TestAModuleIsNotGivenAPortTheMachineAlreadyHolds(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "some-service")
|
|
ctx := t.Context()
|
|
|
|
// What the machine says it raised for itself: the store, the broker, the control plane.
|
|
if err := inv.RecordCarried(ctx, node, []int{5432, 5671, 8080, 20000, 20001}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got, err := inv.PortFor(ctx, node, "some-service", 5432, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, held := range []int{5432, 5671, 8080, 20000, 20001} {
|
|
if got.Machine == held {
|
|
t.Fatalf("it was given %d, which the machine already holds", held)
|
|
}
|
|
}
|
|
if got.Machine != 20002 {
|
|
t.Errorf("expected the lowest free one, 20002, and got %d", got.Machine)
|
|
}
|
|
}
|
|
|
|
// A port the protocol fixes, already held by something the mesh did not put there, is refused —
|
|
// and refused here rather than by a container runtime on the machine.
|
|
func TestAFixedPortTheMachineAlreadyHoldsIsRefused(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "mailu")
|
|
ctx := t.Context()
|
|
if err := inv.RecordCarried(ctx, node, []int{25}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := inv.PortFor(ctx, node, "mailu", 25, true)
|
|
if err == nil {
|
|
t.Fatal("a module was given a port something on the machine already holds")
|
|
}
|
|
if !contains(err.Error(), "already runs") {
|
|
t.Errorf("the refusal does not say the machine itself has it: %v", err)
|
|
}
|
|
}
|
|
|
|
// A machine that gave a port back is believed about that too.
|
|
func TestWhatAMachineNoLongerHoldsIsAvailableAgain(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "a-service")
|
|
ctx := t.Context()
|
|
if err := inv.RecordCarried(ctx, node, []int{20000}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RecordCarried(ctx, node, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := inv.PortFor(ctx, node, "a-service", 1234, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Machine != 20000 {
|
|
t.Errorf("a port the machine gave back was still reserved: got %d", got.Machine)
|
|
}
|
|
}
|
|
|
|
// Unassigning a module gives its ports back — the fixed ones are what make this matter.
|
|
//
|
|
// Held past unassignment, port 25 stays claimed in the name of a mail system that is gone, and
|
|
// every mail system after it is refused by a ghost. Found in review: ReleasePorts existed, was
|
|
// documented "for when it is unassigned", and was called by nothing.
|
|
func TestUnassigningReleasesTheModulesPorts(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "mailu", "other-mail")
|
|
ctx := t.Context()
|
|
if err := inv.Assign(ctx, node, "mailu"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.PortFor(ctx, node, "mailu", 25, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.Unassign(ctx, node, "mailu"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.Assign(ctx, node, "other-mail"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.PortFor(ctx, node, "other-mail", 25, true); err != nil {
|
|
t.Fatalf("port 25 is still held in the name of a module that was unassigned: %v", err)
|
|
}
|
|
}
|