plans: row 87 round 7 makes the leftover rule wait by id until each core container is gone and names the start order (#1581)

Answers Darkwing's round 6 review (28884) and Filbert's doc-only
follow-up (28883).

- After docker stop, the rule waits for "no such container" by id,
  bounded by the core stop timeout, then rm -f and the same wait; a
  container still there refuses exit 1, which the unit retries.
- A name conflict is now only a hand-made container without the store's
  labels; it stays exit 3, which the unit does not retry.
- Start order: host.lock, store path, rootless check, leftover rule,
  docker run.
- I6 gains two tests: run straight after a stop boots, and a leftover
  never removed refuses exit 1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-18 09:28:56 -05:00
co-authored by Claude Opus 5.5
parent e1f13a54d2
commit 4c1fee85a5
2 changed files with 31 additions and 9 deletions
@@ -9,7 +9,9 @@ answers their round 2 reviews (Darkwing 28863, Filbert 28864). Round 4
answers their round 3 reviews (Darkwing 28871, Filbert 28870). Round 5
answers Darkwing's round 4 review (28877) and Filbert's doc-only
follow-up on the stop timeout (28876). Round 6 answers Darkwing's round
5 review (28880) and Filbert's doc-only follow-ups (28881).
5 review (28880) and Filbert's doc-only follow-ups (28881). Round 7
answers Darkwing's round 6 review (28884) and Filbert's doc-only
follow-up on the start order (28883).
This doc turns decisions 105 to 108 into interfaces. It doesn't reopen
them. Every claim about container behavior cites a spike in
@@ -151,8 +153,11 @@ fork's exit code (`host.mjs:341-347`), and the start failure path kills
the notifier alone (`host.mjs:252-257`). Containerized mode keeps all of
that by putting each fork's events on the channel.
- **The store path.** Before anything else touches the engine, bus
start makes `<dataRoot>/bus` if it is missing, mode 0700, the way the
- **The start order.** Bus start takes `host.lock`, then sets the store
path, then runs the rootless check (section 4) before the leftover
rule's first engine call, then the leftover rule, then `docker run`.
- **The store path.** Right after `host.lock`, bus start makes
`<dataRoot>/bus` if it is missing, mode 0700, the way the
broker's `Store` does today (`packages/bus/src/store.mjs:126`), which
only the broker does now. The host makes only `bus-host/`
(`host.mjs:80`, `:120`), so on a fresh install nothing else would. Bus
@@ -175,9 +180,13 @@ that by putting each fork's events on the channel.
store while one exists in any state. The host reads the forks' exit
codes from the channel, not from the container, so `--rm` loses
nothing it reads.
- **A name conflict.** The leftover rule below runs first, so a conflict
means a container with the name appeared after the list, or one the
engine is still removing. The `docker run` client then exits before
- **A name conflict.** The leftover rule below runs first and waits
until every container it listed is gone, so a conflict means a
container holds the name without the store's labels, which only a
hand-made container would. That needs the operator, so the refusal
is exit 3, which the unit doesn't retry
(`packages/cli/systemd/mosaic-bus.service.in:33`). The `docker run`
client then exits before
any line, and the engine's error goes to the host log through the
client's inherited stderr. The host doesn't parse that text. Bus start
refuses with exit 3, `core container mosaic-core-<store hash> did not
@@ -267,9 +276,18 @@ that by putting each fork's events on the channel.
core container starts, bus start lists containers labelled
`mosaic.kind=core` and `mosaic.store=<store path>` through the
endpoint helper. It runs `docker stop -t <core stop timeout>` on each,
45 s at today's value, which covers both of the entry's close waits,
then `docker rm -f` on any that `--rm` hasn't removed, and logs each to
the host log. `host.lock` is held, so no live host owns a leftover. A
45 s at today's value, which covers both of the entry's close waits.
`--rm` removes a container after `stop` returns, and `docker rm -f`
during that removal errors instead of waiting, so the rule then waits
until each listed container is gone, by id, bounded by the core stop
timeout. Gone means the endpoint answers "no such container" for that
id. A container still there at the bound gets `docker rm -f` and the
same bounded wait. One still there after that makes bus start refuse
with exit 1, `leftover core container <id> not removed`, which the
unit retries after `RestartSec`, since an engine still removing is a
passing state. Each step goes to the host log. `docker run` starts
only once every listed container is gone. `host.lock` is held, so no
live host owns a leftover. A
leftover that stopped on SIGTERM has removed `writer.lock`. One that
`rm -f` killed leaves it, and the start refuses `writer-locked`, as it
does today after a broker dies hard. The lock is an exclusive create in
@@ -486,6 +486,10 @@ and no document states the limits in design section 10.
bus start;
- on a fresh data root, bus start makes `<dataRoot>/bus` at 0700
before it resolves the store path;
- `docker run` straight after a leftover's stop boots without a name
conflict: the rule waits until the stopped container is gone;
- a leftover that is never removed in the bound makes bus start
refuse with exit 1 and log its id;
- a container that already holds the core name but not the store's
labels makes bus start refuse with exit 3 and the message naming
the name and the client's exit code; `host.lock` is given back,