The playbook, the README and the status skill knew five statuses; the cycle check knew a sixth, 'fixed', and not 'wontfix'. Eleven issues sat in the sixth for weeks with their fixes shipped, one step short of closed. They are resolved; the check refuses the word from now on and accepts the one the playbook allows.
100 lines
5.1 KiB
Markdown
100 lines
5.1 KiB
Markdown
---
|
|
status: resolved
|
|
opened: 2026-09-01
|
|
located-in: [mesh-control, mesh-host]
|
|
fixed-by: mesh-control 1f5b70a, 41f7c51; mesh-host b91342a
|
|
amended-design: 02-DECISIONS/0038-the-mesh-assigns-the-port.md
|
|
---
|
|
|
|
# 028 — Two things want one port, and nothing says so until the machine
|
|
|
|
## Symptom
|
|
|
|
The database module cannot start on a machine that runs the control plane:
|
|
|
|
```
|
|
Bind for 127.0.0.1:5432 failed: port is already allocated
|
|
```
|
|
|
|
The mesh keeps its own store on that machine, from the bundle, and it holds 5432. The module
|
|
publishes 5432 too. Everything up to the machine is content: it resolves, it composes, it is
|
|
pushed, and it is applied — the container is simply the one resource that fails.
|
|
|
|
## Why nothing catches it
|
|
|
|
**The substrate is not a module.** It arrives from the bundle a host carries, before there is a
|
|
mesh to ask. So the control plane has never heard of `mesh-store` and does not know it holds a
|
|
port. Resolution can compare modules against each other and cannot compare a module against the
|
|
thing the mesh is built on.
|
|
|
|
**And nothing compares modules against each other either.** A port is exclusive on a machine in
|
|
exactly the way a claim is — one seat, one display server, one artifact store — and the mesh has a
|
|
mechanism for that, which ports do not use. Two modules both publishing 5432 would meet the same
|
|
wall, one machine later.
|
|
|
|
## It has been met before, and worked around
|
|
|
|
The end-to-end test that exercises a real database publishes `5433:5432` rather than `5432:5432`.
|
|
The workaround is right there, inline, with no note saying why — which is how a constraint becomes
|
|
folklore.
|
|
|
|
## What a fix has to settle
|
|
|
|
- **Whether a module should publish to the machine at all.** Consumers reach a provider by the
|
|
machine's address and the port it *serves*, so publishing is what makes that true. An alternative
|
|
is that they reach it on the module's own network by name, and nothing is published — which
|
|
changes what `serves` means and is a larger decision than it looks.
|
|
- **Where the substrate's ports are written down.** Whatever compares them needs to know what the
|
|
bundle holds. The bundle is a list of pinned references; what those containers bind is not in it.
|
|
- **What a refusal should say.** *5432 is held by the mesh's own store on this machine* is a useful
|
|
sentence. *Port is already allocated*, arriving from a container runtime three layers down, is
|
|
not.
|
|
|
|
## Not the same as a firewall rule
|
|
|
|
`listens` already says which ports a module accepts on, and filtering is computed from it. That is
|
|
about what may reach a port from elsewhere. This is about two things on one machine wanting to own
|
|
the same one, which `listens` does not model and could not answer.
|
|
|
|
## Answered in principle
|
|
|
|
[ADR 0038](../../02-DECISIONS/0038-the-mesh-assigns-the-port.md), proposed the same day: **the mesh
|
|
assigns the machine-side port and a module does not care.** A module cannot choose well, because it
|
|
is written once and assigned anywhere — any number it picks is a guess about a machine it has never
|
|
seen.
|
|
|
|
A port fixed by its protocol — mail on 25, submission on 587 — becomes a **claim**, which is the
|
|
mechanism the mesh already has for what is singular on a machine. Two modules wanting 25 is the
|
|
same shape as two wanting the seat, and earns the same refusal at assignment rather than at apply.
|
|
|
|
The record also names what this issue missed: the same number is written **three times** in every
|
|
module — once for the rule set, once for what a consumer is told, once for what the runtime
|
|
publishes — and nothing checks that they agree. A module whose `serves` and whose container
|
|
disagreed would hand every consumer a port that answers nothing.
|
|
|
|
## Fixed
|
|
|
|
**The mesh assigns the machine-side port**, from a high unprivileged range, recorded per machine
|
|
and module and kept once chosen. A module says the port its software uses, once, in `listens`. The
|
|
container's mapping, the rule set, and what a consumer is told are all derived from the assignment
|
|
— so the three copies that agreed only because one person wrote them are now one fact.
|
|
|
|
**A port the protocol fixes says so**, and is then a claim: one holder per machine, and the second
|
|
refused by name at assignment rather than by a container runtime at apply.
|
|
|
|
**And the machine says what it already holds.** This was the half that made the issue: the
|
|
substrate is not a module, so nothing in the mesh had heard of the store or the broker. The host
|
|
already distinguished what it carried from what the mesh sent — that distinction exists so the two
|
|
never remove each other — and now records what each resource binds and reports the carried ones.
|
|
The allocator treats those as taken.
|
|
|
|
What the declaration binds, not what is open: a machine's open ports are a moving target, and
|
|
assigning around those would mean a port that was free when it was asked for and taken when it was
|
|
used.
|
|
|
|
## What it does not settle
|
|
|
|
The question underneath, unchanged: **whether a module should publish to the machine at all**.
|
|
Assignment makes publishing safe without making it necessary, and consumers reaching a provider on
|
|
the module's own network by name would make the question moot for anything inside the mesh.
|