plans: row 87 round 6 gives the store one real-path spelling, starts each core fork on its first line and states the name-conflict refusal (#1581)
Answers Darkwing's round 5 review (28880) and Filbert's doc-only follow-ups (28881). - connected follows the channel from the wrapper's creation, as a forked child's does; the entry starts the broker on boot and the notifier on start, so host.mjs:217 and :244 never wait on a pid line. - Bus start makes <dataRoot>/bus at 0700 and takes its real path; the bind source, mosaic.store label, leftover filter and hostStatus all use it. - A name conflict refuses with exit 3, names the container and the client's exit code, gives host.lock back, and removes nothing. - The host.pids test list names what each test does with the pid. - I6 gains four lifecycle tests for these. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b04014e592
commit
80ebe2afc4
2 files changed
+86
-35
No files matched your search
@@ -8,7 +8,8 @@ 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). Round 5
|
||||
answers Darkwing's round 4 review (28877) and Filbert's doc-only
|
||||
follow-up on the stop timeout (28876).
|
||||
follow-up on the stop timeout (28876). Round 6 answers Darkwing's round
|
||||
5 review (28880) and Filbert's doc-only follow-ups (28881).
|
||||
|
||||
This doc turns decisions 105 to 108 into interfaces. It doesn't reopen
|
||||
them. Every claim about container behavior cites a spike in
|
||||
@@ -150,18 +151,39 @@ 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
|
||||
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
|
||||
start then takes the real path of `<dataRoot>/bus`. That one string,
|
||||
the store path, is the store's only spelling in containerized mode. It
|
||||
is the source of the store's bind mount, the `mosaic.store` label, the
|
||||
leftover rule's filter and `hostStatus`'s filter. `Store`'s `safe()`
|
||||
checks only the leaf, so a data root under a symlinked parent has two
|
||||
spellings. With the real path everywhere, both spellings reach the
|
||||
same name and the same label.
|
||||
- **Name and labels.** The host runs the core container with
|
||||
`--name mosaic-core-<store hash>`, `--label mosaic.business=<business>`,
|
||||
`--label mosaic.kind=core`, `--label mosaic.store=<dataRoot>/bus` and
|
||||
`--label mosaic.kind=core`, `--label mosaic.store=<store path>` and
|
||||
`--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 store path. 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.
|
||||
- **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
|
||||
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
|
||||
start (docker run exited <code>); see the host log`, gives `host.lock`
|
||||
back and starts nothing else. It never removes a container it didn't
|
||||
list under the store's labels.
|
||||
- **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()`
|
||||
@@ -175,11 +197,16 @@ that by putting each fork's events on the channel.
|
||||
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. 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`
|
||||
`{from, exit: {code, signal}}` once that fork has exited. The entry
|
||||
starts a fork when the first line for it arrives: the broker on
|
||||
`boot`, the notifier on `start`. It passes that line to the fork as its
|
||||
first IPC message, so a line never waits for a fork that isn't there.
|
||||
No notifier line means no notifier fork, as today when the business
|
||||
has no notifier. Once a fork has started, the entry writes `{from,
|
||||
pid}` for it, before any other line from that fork, 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'}`
|
||||
@@ -187,31 +214,40 @@ that by putting each fork's events on the channel.
|
||||
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.
|
||||
- **`connected`.** A wrapper's `connected` follows the channel, as a
|
||||
forked child's does today: it is true from the moment the host makes
|
||||
the wrapper. The broker's wrapper is made when the `docker run` client
|
||||
starts, and the notifier's where `host.mjs:242` forks today. The host
|
||||
sends `boot` and `start` right after (`host.mjs:217`, `:244`), and
|
||||
those writes go through, because the entry starts the fork on that
|
||||
line. The `{from, pid}` line changes nothing in `connected`; it gives
|
||||
the log its pid. `connected` 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
|
||||
`process.kill` would signal an unrelated host process. Four tests use
|
||||
`host.pids` as host pids: `host.test.mjs:202-205` reads each pid's
|
||||
`/proc` cmdline and environ, `:227` sends the notifier SIGKILL,
|
||||
`:546-563` sends the broker SIGSTOP and SIGCONT, and `:612` finds the
|
||||
broker's child process by pid to send it IPC. They 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
|
||||
line and exits 1. The host gets the first exit line, logs
|
||||
stops the other, if it started, the way `close()` does, waits up to the
|
||||
host's close timeout, sends SIGKILL if it hasn't exited by then, writes
|
||||
its exit line and exits 1. The host gets the first exit line, logs
|
||||
"`<name>` exited", and stops with exit 1, as `host.mjs:355-364` does.
|
||||
When the host stops both forks, the entry exits 0 once both have
|
||||
exited 0, and 1 otherwise.
|
||||
When the host stops the forks, the entry exits 0 once each fork it
|
||||
started has exited 0, and 1 otherwise.
|
||||
- **The client ends first.** If the `docker run` client exits, or its
|
||||
stdout closes, before a fork's exit line arrives, the host treats that
|
||||
fork as exited with signal `SIGKILL` and stops with exit 1.
|
||||
@@ -229,7 +265,7 @@ that by putting each fork's events on the channel.
|
||||
design doesn't rely on them, because of the next rule.
|
||||
- **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
|
||||
`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
|
||||
@@ -247,7 +283,7 @@ that by putting each fork's events on the channel.
|
||||
broker would show as "a broker died hard". In containerized mode
|
||||
`hostStatus` ignores the lock's pid and asks the engine instead. The
|
||||
lock is live when a container labelled
|
||||
`mosaic.store=<dataRoot>/bus` and `mosaic.kind=core` is running, and
|
||||
`mosaic.store=<store path>` and `mosaic.kind=core` is running, and
|
||||
dead when none is. If the endpoint doesn't answer, it reports the
|
||||
lock as unknown, never dead. The `socket:` line checks `sock/agent`
|
||||
and `sock/human` (section 5) instead of `bus/broker.sock`. I6 owns
|
||||
|
||||
@@ -403,8 +403,9 @@ and no document states the limits in design section 10.
|
||||
and `packages/cli/src/notifier-process.mjs` don't change.
|
||||
- `packages/cli/src/host.mjs` for the channel's fork wrapper, the
|
||||
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`
|
||||
containerized mode, the store path (making `<dataRoot>/bus` and
|
||||
resolving it), the name-conflict refusal, 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
|
||||
@@ -475,6 +476,20 @@ and no document states the limits in design section 10.
|
||||
- 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;
|
||||
- `boot`, sent right after the client starts and before any `{from,
|
||||
pid}` line, reaches the broker and gets its reply well inside
|
||||
`BOOT_TIMEOUT_MS`; a business without a notifier gets no notifier
|
||||
fork, and close still returns 0;
|
||||
- a data root under a symlinked parent: the core container's
|
||||
`mosaic.store` label is the real path, and a leftover started
|
||||
under the other spelling is found, stopped and logged at the next
|
||||
bus start;
|
||||
- on a fresh data root, bus start makes `<dataRoot>/bus` at 0700
|
||||
before it resolves the store path;
|
||||
- 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,
|
||||
and that container is left as it was;
|
||||
- `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