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

5.1 KiB

status, opened, located-in, fixed-by, amended-design
status opened located-in fixed-by amended-design
resolved 2026-09-01
mesh-control
mesh-host
mesh-control 1f5b70a, 41f7c51; mesh-host b91342a 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, 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.