Appearance
Shutdown semantics
This page covers two related things: how the Host shuts itself down, and the launch-readiness work that made shutdown and the subsequent restart safe — atomic writes, parse-and-skip bootstrap, idempotent seed, journald cap, and StartLimitBurst hardening. The work lives under WORKSTREAMS/launch-readiness-atomic-writes-and-bootstrap/.
Host stop sequencing
matrix stop (CLI: cli.ts:140-143) calls stopRunningHost (cli.ts:960-998). For a live host.status.json:
- Try graceful supervisor shutdown. Send
shutdowntohost.supervisorover NATS with the configured timeout (default 10s). The supervisor schedules its ownMatrixHostService.stop()onsetImmediateand replies immediately (matrix-host-service.ts:838-844). - Wait for pid exit up to
timeoutMs. - If the pid is still alive after the deadline: SIGTERM.
- Wait another 2s.
- If still alive: SIGKILL.
MatrixHostService.stop() (matrix-host-service.ts:106-123) does the graceful work in this order:
- Stop every managed runtime (parallel,
stopRuntimeProcessper id). - Stop the supervisor control server (close NATS subscription).
- Stop the embedded NATS sibling (only if
mode: "embedded"and the Host owns the sibling — wrapper-managed siblings are not the Host's responsibility). - Mark
status.status = "stopping"atomically. - Clear
host.status.jsonandhost.pid.
Each runtime's stop follows the Stop runtime sequence: graceful runtime.shutdown first, SIGTERM after 5s grace, SIGKILL after another 2s.
NATS sibling teardown (wrapper installs)
The wrapper supervises NATS as a fully detached sibling. hivecast stop (hivecast.mjs:1765-1770) calls stopHostProduct, which in turn calls stopNatsSibling (hivecast.mjs:687-723):
- Read pidfile; if no pid or pid not alive, declare "already stopped."
- SIGTERM, wait up to 3s. NATS flushes JetStream and unbinds ports.
- If still alive: SIGKILL, wait up to 1s.
- Remove the pidfile.
The 3-second SIGTERM grace is intentional: an immediate SIGKILL leaves JetStream mid-write and the next NATS start can race against still-bound ports — see the comment at hivecast.mjs:700-704.
Recovery work — what makes restart safe
1. Atomic JSON writes
projects/matrix-3/packages/host-service/src/util/atomic-write.ts:17-48 implements atomicWriteJson and atomicWriteText:
ts
function atomicWriteText(filePath: string, value: string): void {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
let fd: number | null = null;
try {
fd = fs.openSync(tmpPath, 'wx', 0o644);
fs.writeSync(fd, value);
fs.fsyncSync(fd);
} finally { /* close */ }
fs.renameSync(tmpPath, filePath);
// best-effort directory fsync for crash durability
}Every Host JSON write — host.json, host.status.json, every runtime.json, every runtime-env/*.environment.json — goes through this path. A crash mid-write cannot leave a half-written file. The worst case is a leftover .tmp.<pid>.<ts> next to the real file, which is harmless.
2. Parse-and-skip bootstrap
HostStateStore.listRuntimeRecordsAndCorrupt (host-state-store.ts:106-142) replaces a "throw on first bad record" shape with a {records, corrupt} shape. Reasons:
- A single zero-byte
runtime.json(the original 22-hour crash-loop bug — see commitd270d625) used to crash bootstrap, which crashed the systemd unit, which kept restarting forever. - Now bootstrap succeeds with the healthy records and reports the corrupt list;
hivecast seedre-seeds empty records on the next start; the Host stays up.
3. Idempotent seed
hivecast seed (hivecast.mjs:494-554) re-seeds the bundled package store from the wrapper's dist/node_modules/, scans existing runtime records, and emits a structured report:
json
{
"ok": true,
"home": "...",
"seeded": true,
"runtimeRecords": {
"healthy": 9,
"corrupt": [
{ "runtimeId": "...", "path": "...", "kind": "empty", "size": 0 }
]
}
}Running seed repeatedly is safe.
4. journald cap
The .deb postinst installs /etc/systemd/journald.conf.d/hivecast.conf with SystemMaxUse=2G, SystemKeepFree=4G, RateLimitIntervalSec=30s, RateLimitBurst=10000. See Logs. This is the disk-safety backstop for runaway log emission during a crash loop.
5. StartLimitBurst / StartLimitIntervalSec hardening
Both hivecast-nats.service and hivecast-host.service ship with:
ini
StartLimitBurst=5
StartLimitIntervalSec=60(build-deb-installer.js:111-112 and 139-140.) systemd will refuse to restart the unit more than 5 times in 60 seconds, breaking the runaway-restart pattern that originally filled disks. After hitting the limit, an operator must systemctl reset-failed <unit> and investigate.
Orphan recovery on next start
Even with all of the above, a Host process that crashed while runtimes were live needs to reconcile. _recoverOrphanedAutoRuntimeRecordsBeforeStart (matrix-host-service.ts:578-622) runs first thing in start():
- For every record with
startup: "auto"and a live status:- If the recorded pid is alive but its commandline doesn't match, mark
failedwith astaleReason. - If the pid is alive and matches, SIGTERM, then SIGKILL after 2s. Mark the record
stoppedwithorphanRecoveredAt. - If the pid refuses to die, refuse to start.
- If the recorded pid is alive but its commandline doesn't match, mark
- Then
_autoStartPersistedRuntimeswalks the records in priority order and respawns them fresh.
This is what makes systemctl restart hivecast-host.service safe even when the prior Host left some children behind.
See also
- Stop runtime
- Restart runtime
- Host lifecycle
- Failure modes
WORKSTREAMS/launch-readiness-atomic-writes-and-bootstrap/— the full brief on this work.