plans: row 87 round 5 keys the core container on the store, sets the stop timeout to cover both close waits, and defines connected and host.pids (#1581)
Answers Darkwing's round 4 review (comment 28877) and Filbert's
doc-only follow-up (28876).
- P: the core container's name is mosaic-core-<16 hex of SHA-256 of the
real path of <dataRoot>/bus>, keyed on the store like writer.lock, so
two data roots with the same business don't collide.
- Q: close() waits CLOSE_TIMEOUT_MS twice (host.mjs:30, 343, 347). The
host passes MOSAIC_CLOSE_TIMEOUT_MS to the entry; the core stop
timeout is two close timeouts plus 5 s (45 s today), used by the
leftover rule's docker stop -t.
- R: {from, pid} start line; connected turns false on the exit line,
client stdout close or exit, or a failed stdin write; host.pids is
null for both forks in containerized mode and adds container; the
pid-signalling host tests stay local, I6 uses docker exec kill.
- Nit: firstReply (129) and ended (148) cited apart from watchChildren
(159-165).
- I6 tests and design section 9 add the slow-notifier leftover, two data
roots, the missing-timeout refusal and connected.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -6,7 +6,9 @@ design. The I2 to I6 briefs are in
|
||||
`docs/plans/2026-10-18_containerized-install-rows.md`. Round 2 answers
|
||||
Filbert's review (comment 28851) and Darkwing's (comment 28852). Round 3
|
||||
answers their round 2 reviews (Darkwing 28863, Filbert 28864). Round 4
|
||||
answers their round 3 reviews (Darkwing 28871, Filbert 28870).
|
||||
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).
|
||||
|
||||
This doc turns decisions 105 to 108 into interfaces. It doesn't reopen
|
||||
them. Every claim about container behavior cites a spike in
|
||||
@@ -149,26 +151,60 @@ the notifier alone (`host.mjs:252-257`). Containerized mode keeps all of
|
||||
that by putting each fork's events on the channel.
|
||||
|
||||
- **Name and labels.** The host runs the core container with
|
||||
`--name mosaic-<business>-core`, `--label mosaic.business=<business>`,
|
||||
`--name mosaic-core-<store hash>`, `--label mosaic.business=<business>`,
|
||||
`--label mosaic.kind=core`, `--label mosaic.store=<dataRoot>/bus` and
|
||||
`--rm`. The fixed name means the engine refuses a second core container
|
||||
for the business 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.
|
||||
`--rm`. `<store hash>` is the first 16 hex digits of the SHA-256 of
|
||||
the real path of `<dataRoot>/bus`. The name is keyed on the store, as
|
||||
`writer.lock` is, not on the business: two data roots that run the
|
||||
same business get two names. The `packages/cli` suite runs its test
|
||||
files in parallel, and many of them use the business `acme`, each with
|
||||
its own data root. The fixed name means the engine refuses a second
|
||||
core container for the 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.
|
||||
- **The close timeout.** `close()` waits `CLOSE_TIMEOUT_MS` (20 s,
|
||||
`host.mjs:30`) for the notifier, then again for the broker
|
||||
(`host.mjs:343`, `:347`). So stopping both forks the way `close()`
|
||||
does can take two close timeouts. The host passes the value to the
|
||||
entry as `MOSAIC_CLOSE_TIMEOUT_MS` on the `docker run` command line,
|
||||
taken from the same constant, which I6 exports. The entry refuses to
|
||||
start without it. The core stop timeout is two close timeouts plus 5
|
||||
s, rounded up to whole seconds: 45 s at today's value. The leftover
|
||||
rule below passes it to `docker stop -t`.
|
||||
- **Lines.** A line from the host is `{to, msg}`, where `to` is `broker`
|
||||
or `notifier` and `msg` is the IPC message today's host sends, or
|
||||
`{to, signal}`, which the entry delivers as `kill(signal)` on that
|
||||
fork. A line to the host is `{from, msg}` for an IPC reply, or
|
||||
`{from, exit: {code, signal}}` once that fork has exited. The host
|
||||
wraps each fork in an object with `send`, `kill`, `connected`,
|
||||
`exitCode`, `signalCode` and an `exit` event driven by those lines.
|
||||
`firstReply`, `ended`, `watchChildren` (`host.mjs:159-165`) and
|
||||
`{from, exit: {code, signal}}` once that fork has exited. Before
|
||||
either, the entry writes `{from, pid}` once the fork has started,
|
||||
where `pid` is in the container's pid namespace. The host wraps each
|
||||
fork in an object with `send`, `kill`, `connected`, `exitCode`,
|
||||
`signalCode` and an `exit` event driven by those lines. `firstReply`
|
||||
(`host.mjs:129`), `ended` (`:148`), `watchChildren` (`:159-165`) and
|
||||
`close()` keep their logic:
|
||||
- `close()` sends `{op: 'stop'}` to the notifier and `{op: 'close'}`
|
||||
to the broker, as today, and `ended()` gets each code from the exit
|
||||
line;
|
||||
- the start failure path's `notify.kill('SIGTERM')` becomes
|
||||
`{to: 'notifier', signal: 'SIGTERM'}`.
|
||||
- **`connected`.** A wrapper's `connected` turns true on the fork's
|
||||
`{from, pid}` line. It turns false, and never back, on the first of
|
||||
three events: the fork's exit line, the client's stdout closing or the
|
||||
client exiting, or a write to the client's stdin failing. Once it is
|
||||
false, `send` and `kill` write nothing and return false. The host's
|
||||
`connected` checks before a send (`host.mjs:256`, `:342`, `:346`)
|
||||
therefore skip a fork whose channel is gone, as they do today when a
|
||||
child has disconnected.
|
||||
- **`host.pids`.** In containerized mode both entries are null, and the
|
||||
object adds `container`, the core container's name. The host doesn't
|
||||
know a fork's pid in its own namespace. A container pid passed to
|
||||
`process.kill` would signal an unrelated host process. The tests that
|
||||
signal a fork by pid (`host.test.mjs:227`, `:546-563`, `:612`) stay in
|
||||
local mode, where the host logic they test is unchanged. The host logs
|
||||
each fork's container pid from its `{from, pid}` line. I6's lifecycle
|
||||
tests reach a fork with
|
||||
`docker exec <container> kill -<signal> <pid>`, using the pid from
|
||||
that log line.
|
||||
- **One fork exits alone.** The entry writes that fork's exit line, then
|
||||
stops the other the way `close()` does, waits up to the host's close
|
||||
timeout, sends SIGKILL if it hasn't exited by then, writes its exit
|
||||
@@ -194,7 +230,8 @@ that by putting each fork's events on the channel.
|
||||
- **Leftovers at bus start.** After `host.lock` is taken and before the
|
||||
core container starts, bus start lists containers labelled
|
||||
`mosaic.kind=core` and `mosaic.store=<dataRoot>/bus` through the
|
||||
endpoint helper. It runs `docker stop -t <close timeout>` on each,
|
||||
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
|
||||
leftover that stopped on SIGTERM has removed `writer.lock`. One that
|
||||
@@ -853,6 +890,12 @@ request, a slow client and a connect to a port that isn't listed.
|
||||
old broker is gone before the next one boots, and the test records
|
||||
whether EOF reached the entry;
|
||||
- a running leftover is stopped and removed at bus start;
|
||||
- a leftover whose notifier is slower than one close timeout still
|
||||
exits on SIGTERM within the core stop timeout and removes
|
||||
`writer.lock`;
|
||||
- two data roots with the same business get two core containers;
|
||||
- the entry refuses to start without `MOSAIC_CLOSE_TIMEOUT_MS`, and
|
||||
`connected` turns false after an exit line or the client's exit;
|
||||
- `bus status` shows a running broker's lock as live, a lock left
|
||||
after `rm -f` as dead, and the lock as unknown when the endpoint
|
||||
doesn't answer.
|
||||
|
||||
@@ -402,7 +402,9 @@ and no document states the limits in design section 10.
|
||||
for the host channel (design section 3). `packages/bus/src/process.mjs`
|
||||
and `packages/cli/src/notifier-process.mjs` don't change.
|
||||
- `packages/cli/src/host.mjs` for the channel's fork wrapper, the
|
||||
leftover removal at bus start and `hostStatus`, and the `bus status`
|
||||
exported close timeout and the core stop timeout, `host.pids` in
|
||||
containerized mode, the leftover removal at bus start and
|
||||
`hostStatus`, and the `bus status`
|
||||
lines in `packages/cli/src/cli.mjs` (design section 3, "The core
|
||||
container's lifecycle").
|
||||
- `compose.yaml`, `scripts/run-task.sh` and the worker adapter files it
|
||||
@@ -464,6 +466,15 @@ and no document states the limits in design section 10.
|
||||
start booted or refused `writer-locked`;
|
||||
- a running leftover core container is stopped, removed and logged
|
||||
at bus start, and a fresh one boots;
|
||||
- a leftover whose notifier takes longer than one close timeout to
|
||||
stop still exits on SIGTERM within the core stop timeout, and
|
||||
`writer.lock` is gone afterwards;
|
||||
- two data roots running the same business start two core
|
||||
containers with different names, and stopping one leaves the
|
||||
other running;
|
||||
- the entry refuses to start without `MOSAIC_CLOSE_TIMEOUT_MS`;
|
||||
- a wrapper's `connected` turns false after the fork's exit line
|
||||
and after the client exits, and a later `send` writes nothing;
|
||||
- `mosaic bus status` shows a running broker's lock as live, a lock
|
||||
left after `docker rm -f` as dead, and the lock as unknown when
|
||||
the endpoint doesn't answer;
|
||||
|
||||
Reference in New Issue
Block a user