Files
hq/04-ISSUES/130-undeclaring-a-service-stops-it/00-report.md
T
jschoubben ce6ae943b7 Merge main: renumber this branch's records around the trunk's
Both lines of work numbered from the same point, so four decision records and one design
document existed twice with different content. The trunk keeps its numbers and this branch
yields — the only rule that scales, because the trunk's are already cited by what merged
before them.

  0117 the bus is the only broker        -> 0125
  0118 a module declares its own seats   -> 0126
  0119 amqp is a provision, not the bus  -> 0127
  0120 the mesh bus is required          -> 0128
  0123 a seat carries its role's protocol -> 0129
  0124 the predecessor is ending          -> 0130
  design 29, what a module declares       -> design 32

Applied to the code repositories too, because a stale reference is worse when numbers
collide than when they dangle: the reader lands on a real record that decided something
else.

Two reconciliations the merge forced, both real:

**0110 was marked wholly superseded and was not.** Its successor says in as many words that
everything 0110 decided about what a seat *is* stands untouched — and two records that
landed on the trunk rest on exactly that part. So it is accepted again, extended rather than
replaced, with a note saying which of its claims moved and where.

**A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of
compiled code into a table the controller owns. This branch had added what a role accepts,
emits and serves to the Go slice. The decision is unaffected and the mechanism is better for
it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own
argument applied to what this branch added.

One check still fails and it fails on main too: a record resting on ADR 0112 while that is
still 'proposed'. Left alone — it is not this merge's to answer.
2026-09-27 18:23:41 +02:00

3.8 KiB

status, opened, located-in, amended-design
status opened located-in amended-design
located 2026-09-27
mesh-host internal/apply/apply.go (remove)
02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md

130 — undeclaring a service stops it, even one the mesh only reloads or only keeps running

What was observed

Reviewing the uplink modules (ADR 0125) found that the host's remove path stops every service resource that is no longer declared: SetServiceState(..., "stopped"), reported as "stopped; the unit file is not the host's to delete". store.Orphans matches by id alone. So any of these stops the unit:

  • the module is unassigned — by mistake, or to switch it for another;
  • the node is sent a deliberately-empty declaration (issue 127);
  • a later catalogue version renames the resource's id.

That is right for a service the mesh brought into being. It is wrong for a unit the mesh declares only to act on — and the catalogue already has two:

  • The private network declares docker.service (registry-trust-reload, state running) so that a change to the registry trust reloads the runtime (ADR 0102). Unassigning the private network stops the container runtime, and every container on the machine with it — including ones the mesh does not manage.
  • The sshd module declares sshd.service. Unassigning it stops the machine's ssh daemon: the lockout the same module's listens rule says a firewall must never arrange.

The uplink modules would have added a third and a fourth: unassigning the network manager's module would have stopped the network manager, taking the machine off the only link the mesh reaches it by.

What would have prevented it

  • A service resource that says the unit's lifecycle is the machine's: declared with no state, the mesh never starts, stops, enables or disables it; it only reloads or restarts a running unit when a trigger changes; undeclared, it is left exactly as it is. (Being built on mesh-host feat/a-file-written-into-a-marked-block for the uplink modules.)
  • Then: registry-trust-reload declared that way (the runtime is the machine's), and the sshd module's service too — a machine's ssh daemon outlives any module that configures it.
  • A plan or unassign preview that names every unit an undeclare will stop, so the consequence is read before it happens.

Resolution

ADR 0126: undeclaring removes what the mesh made and gives back what it changed. The host records the state it first found a unit in, and undeclaring returns the unit to it — a unit found running (the container runtime, sshd, a network manager) is left running; one the mesh started (the packet filter a converge loaded) is stopped again; nothing is started on the way out; a record from before the host kept what it found leaves the unit alone. That covers the runtime, sshd and the uplink modules at once, without each module opting out; the private network and the sshd module need no change.

A first draft — never stop a unit the mesh did not create — was rejected while implementing it: returning a converged node to adopted unloads the mesh's filter by exactly this path.

Found on the way: an undeclared process failed every apply on its node (remove had no case for it). Now removed with its unit, timer and bundle — the mesh's own code. user and archive have the same gap and are left for their own decisions: removing a login or unpacked files is not something to settle in passing.

The unassign preview is partly answered — the host's plan names each unit it will stop — and the controller's side is left open.