Files
jschoubben 36d9b38a0d An issue is open, diagnosing, located, resolved or wontfix — nothing else
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.
2026-09-21 17:38:17 +02:00

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.