This document describes the system as it is RIGHT NOW, on this branch. It is a living map of how processes talk, who reaps whom, and what files exist and who reads them.
Rules for this file:
- Current state only. It must match the code as committed. When IPC, the process tree, reaping, sockets, or on-disk artifacts change, update this file in the same change.
- Nothing that is gone. Do not describe retired files, removed processes, or old mechanisms. If it no longer exists, it does not appear here.
- Nothing aspirational. Target/end-state design lives in
ARCHITECTURE.md. Planned-but-unbuilt features do not appear here until they exist.
If ARCHITECTURE.md (target) and this file (current) disagree, that gap is the
remaining migration work — this file still describes what is actually true today.
Every process yath starts — the runner, each preload stage, and each test job —
runs under its own Test2::Collector parent. The collector is the parent
process; it execs/forks the real work as its child, records the child's
stdout/stderr/exit as timestamped events into a *.jsonl.zst file, and (for every
process except the runner) its reporter streams transitions to runner.socket.
For a non-test wrap (runner / stage / aux) the collector parent POSIX::_exits
with the wrapped child's exit (Util::collector_exit_code), so reaping that process
carries a meaningful exit. A test-job collector parent instead exits
health-only (Test2::Collector::Runner->spawn_exit_code: 0 if the collector
functioned, non-zero only if the collector itself malfunctioned): the test's verdict
rides its transitions, which the runner decides on the collector's connection EOF
to runner.socket (§5.4), never the exit code. Reaping a test-job collector is
therefore pure zombie cleanup.
- Runner + preload stages run under non-test collectors.
- Test jobs run under test collectors. The test-job reporter connection is
bidirectional (built
read_control): besides streaming transitions it reads the runner's inboundterminatecontrol (the bail/abort message). Its identity preamble carriesjob_id+job_try+run_idso the runner maps the connection (and its EOF) back to the job.
Below, [collector:NAME] → proc means "proc runs under a non-test/test collector
labelled NAME".
yath test COMMAND: client + renderer host
│ connects to runner.socket: submit run + subscribe(run_id)
│ renders via Renderer::Driver (reads each *.jsonl.zst by path)
│
└─ spawn ─ [collector:runner] ─► runner (service; binds runner.socket)
│ with a preload-root: SCHEDULER-ONLY -- holds NO preloaded state, hosts NO stage;
│ in-process scheduler (in-memory State); Runner::Monitor = canonical state;
│ dispatches each task over the ONE channel the hosting stage opened to it
│
├─ spawn ─ [collector:preload-root] ─► preload-root
│ (perl -I... -MTest2::Harness2::Preload=launch,runner.socket -e 1;)
│ │ dials runner.socket as 'preload-root'; handshake: get_preload_list / set_stage_data
│ │ drives a stage host (Test2::Harness2::Preload::Host; rootpid = the REAL runner pid) -- an independent class, NOT a Runner
│ │ HOSTS the base/default/NOPRELOAD stage in-process (dials runner.socket as 'preload-<base>')
│ │
│ └─ fork ─ [collector:stage-<name>] ─► preload stage (binds preload-<name>.socket -- `yath spawn` connects here directly)
│ │ dials runner.socket as 'preload-<name>'; reports up + receives dispatch
│ ├─ fork ─ intermediate (setsid; exits) ─ fork ─ [collector:job] ──► test job (longjump+goto-file via JobLauncher)
│ │ the collector setsid's + DETACHES; it re-parents UP to the runner (subreaper) ······▲ (reaped there)
│ └─ `yath spawn`: fork ─ intermediate (setsid; exits) ─ fork ─ supervisor (NO collector, DETACHED)
│ dials the command's listen socket, recv_fds STDIN/OUT/ERR, fork ─ script child (longjump+goto-file, NO exec)
│ reports the child's raw wait status over the control channel; kill(-pgid) on command EOF
│
├─ spawn ─ [collector:sampler] ─► system-load sampler (ALWAYS spawned, any run)
│ │ Test2::Harness2::Service::Sampler->run (in-process via the collector's run sub)
│ │ binds sampler.socket (unused); dials runner.socket as 'sampler'
│ └─ pushes one-way `system_load` reports (change-gated, 0.2s tick) to the runner
│
└─ no-preload / below-threshold: fork ─ [collector:job] ─► test job (collector fork+EXECs a clean perl; runner's direct child)
The system-load sampler (§4.4) is spawned on every run (preload or not),
right after runner.socket is bound, as a non-test collector whose ChildMonitor
watches the runner (watch_parent_pid); it records to sampler-events.jsonl.zst.
It dials runner.socket as sampler and pushes change-gated system_load reports;
the runner stores the latest snapshot and broadcasts a harness_system transition
to subscribers. The runner reaps it at wind-down (stop_sampler) before closing
its socket, so the sampler's collector (which inherited the runner's std-fd write
ends) does not hold the runner's own collector open past shutdown.
The preload-root level exists only when preloads are configured (and not below
preload_threshold); otherwise the runner forks the test-job collector itself and
the collector fork+execs a clean perl for the test (the no-preload path -- no
goto::file / Long::Jump / in-process launch; the test child does NOT inherit the
runner's %INC). goto::file + Long::Jump live only in the preload tree
(Test2::Harness2::Preload + Test2::Harness2::Runner::JobLauncher), where a
preloaded test must run in-process with its stage's modules already loaded. The
base/default/NOPRELOAD stage runs in-process in the preload-root; named stages
are forked as its children.
Lifespan: the transient runner shuts down and closes runner.socket once the run
is done and its job children are reaped; the command's render loop ends on that
socket EOF. The runner reaps the preload-root at wind-down (stop_preload_root).
yath start (spawns the runner; publishes the runner-socket discovery symlink; then exits)
│
└─ [collector:runner] ─► persistent runner (service; stays up on runner.socket)
│ serialized: one active run at a time
│ scheduler-only when a preload-root hosts the stages
│ Runner::Monitor keyed by run; routes each client only its run
├─ [collector:preload-root] ─► preload-root (dials runner.socket as 'preload-root';
│ │ drives a stage host (Test2::Harness2::Preload::Host), rootpid = real runner)
│ ├─ in-process base/default/NOPRELOAD stage (dials runner.socket)
│ └─ fork ─ [collector:stage-<name>] ─► preload stage
│ │ dials runner.socket as 'preload-<name>';
│ │ binds preload-<name>.socket (`yath spawn` connects here directly)
│ ├─ fork ─ intermediate (setsid; exits) ─ fork ─ [collector:job] (setsid; DETACHES) ──► test job
│ │ the detached collector re-parents UP to the runner (subreaper) and is reaped there ··▲
│ └─ `yath spawn`: fork ─ intermediate (setsid; exits) ─ fork ─ supervisor (NO collector, DETACHED) ─ fork ─ script child (NO exec)
├─ [collector:sampler] ─► system-load sampler (dials runner.socket as 'sampler'; pushes change-gated system_load; reaped at stop)
└─ no-preload: ─ [collector:job] ─► test job (runner's direct child)
clients connecting IN to runner.socket:
yath run submit run(run_id) + subscribe(run_id) → renders its run via Renderer::Driver
yath watch subscribe (no run_id = global) → renders runner/stage output via Renderer::Base
yath status / ps / resources request → Runner::StatusReport reply
yath abort 'truncate' request → the runner halts the queue AND terminates the
running tests itself (abort intent + terminate control, hard-kill
fallback); the command no longer snapshots or signals pids
yath stop 'stop' request (graceful shutdown)
yath reload SIGHUP → the runner's own pid (resolved via the runner-socket
symlink → workdir `PID` file). On a
preload run the (scheduler-only) runner does NOT wind down: its HUP
handler forwards a 'reload_root' request to the base/default stage's
LIVE service channel (the `preload-<base>` peer whose announced pid
equals the preload-root pid). That stage is hosted in the preload-root
process and is the one connection the runner services for the whole
run, so the reload is read promptly; it ends its run loop and re-execs
the whole preload tree from the 'preload-root' Long::Jump frame. The
reload is NOT sent to the preload-root's dormant handshake channel
(which is only serviced at post-run idle, so a reload there would sit
unread, or fire late during shutdown). A queued 'stop' cancels a
pending reload so a stale reload cannot re-exec the tree mid-shutdown.
The runner never self-restarts: it holds no preloaded interpreter
state. A no-preload runner has nothing to reload, so HUP is a no-op (to
pick up code changes a no-preload persistent runner must be stopped and
started again).
yath run/spawn/stop/status/ps/abort/resources/which/watch
discover the runner via a well-known symlink to its runner.socket (resolved
through App::Yath2::Discovery, wrapped by App::Yath2::Pfile): follow the
symlink to the socket + workdir, connect to confirm liveness, and read the workdir
PID file only as the signal fallback for a wedged runner whose socket is
unresponsive. A dangling/connect-fail symlink means the runner is absent (the
stale symlink is cleaned). The persistent runner stays listening until a stop
request.
yath spawn uses the same discovery to find the workdir, then connects
directly to a preload stage (preload-<stage>.socket), bypassing the runner
entirely (see the spawn flow below).
Reaping is local to whichever process forked the (collector-wrapped) child.
Test2::Harness2::IPC is the multi-child reaper for the two genuinely
multi-child forkers -- the runner (its own no-preload job collectors) and
Test2::Harness2::Preload::Host (its forked stages + jobs). The yath test
command, which forks exactly one child (the runner), reaps it inline on
Util::IPC without the controller (§4 table). The runner's
Role::Service::reap_children is a deliberate no-op so it does not race
Test2::Harness2::IPC.
| Forks / spawns | Reaps | Notes |
|---|---|---|
yath test command |
the runner (its collector) | transient only; start/run do not reap the persistent runner. The command owns exactly this one child, so it spawns it on Util::IPC::run_cmd and reaps/signals it with a direct waitpid/kill (no Test2::Harness2::IPC controller instance) |
| runner | the preload-root (its collector); the system-load sampler (its collector); on the no-preload path, the test job (its collector) it forked directly; as a child subreaper, every re-parented detached preload test collector | the preload-root and sampler pids are tracked outside the runner's {+PROCS} (so they never trip IPC::_bring_out_yer_dead's waitpid(-1)) and reaped explicitly at wind-down (stop_preload_root / stop_sampler). The sampler is reaped before the runner closes its socket so its collector (which inherited the runner's std fds) does not stall the runner's own collector on the orphan timeout. The runner is a child subreaper (§4a), so an orphaned detached preload collector re-parents to it; its waitpid(-1) reaper reaps that pid as an unwatched zombie (_bring_out_yer_dead skips it for verdict purposes) and runs only the A3 post-pass health check on it (_reaped_unwatched_pid) |
| preload-root | each preload stage (its collector) it forked | the base/default/NOPRELOAD stage runs in-process in the preload-root and is not forked/reaped |
| preload stage | only the short-lived intermediate of each test-job launch | a preload test launch double-forks + detaches the collector (JobLauncher::launch_via_double_fork): the stage forks an intermediate that setsids and forks the collector parent (which setsids into its own session/group) then exits, orphaning the collector. The stage reaps only the intermediate (a generic watch_pid, pure zombie cleanup) and never watches the collector — that re-parents away (to the runner subreaper, else init). The stage tracks no collector pid (the detached collector reports its own pid to the runner on its handshake); it still forwards any reload. A resource-skip/dummy job (no via) still fork-execs its collector as the stage's direct child and reports that pid |
stop_preload_root must not kill the collector parent (leak fix, chunk 19.4d).
spawn_collector returns the collector parent pid; the actual preload-root is the
-e child it exec'd. stop_preload_root sends a graceful socket stop and reaps
over a generous window; if that does not land in time it leaves the collector
parent alone rather than TERM/KILLing it. Killing the collector parent would
destroy its ChildMonitor (which is exactly what kills the preload-root tree when
the runner vanishes) and orphan the -e child. Instead the runner exits moments
later; the ChildMonitor (watch_parent_pid → runner) then terminates the -e
child + its stage/job descendants, and the collector parent finalizes and exits.
Completion is decided on the collector's connection EOF, not from reaping (§5.4).
A test job's collector streams its verdict transitions (harness_final_state, plus
an early halt transition on a bail) to runner.socket; when the collector exits,
its connection EOFs. The runner decides the outcome on that EOF (after draining the
connection's pending frames): final_state pass ⇒ complete; !pass ⇒ retry while
tries remain else fail; halt ⇒ bail; no final_state ⇒ fail (a terminated job is
recorded aborted, otherwise flagged possible-harness-internal — never a false
pass). The decision is fire-once per (job_id, job_try): a shared ledger the EOF
path and the Runner::Watchdog both set, so an EOF and an abort that race converge to
one completion; a superseded try's late EOF is ignored (stale-try guard). Reaping is
pure zombie cleanup afterward. The Runner::Watchdog still aborts a job whose
dispatch to a stage failed and any still-running jobs at run wind-down.
The reporter is mandatory + a connect timeout bounds the wait (§5.4). Because
EOF + transitions are the only completion signal, a test that ran without a connected
reporter would never complete. So a test-job collector whose runner.socket is
present but whose reporter could not connect fails the job (the collector parent
exits non-zero without running the test) rather than degrading to recorder-only. And
a job the runner marked dispatched/running whose collector never connects within
--collector-connect-timeout (on by default, ~30s) is failed by the per-tick scan
(it would otherwise never EOF). The connect-watch is stamped when the runner first
marks a job dispatched/running and cleared the moment its collector connects.
Bail/abort teardown rides a runner→collector terminate control. On a halt
transition the runner stops dispatching new jobs for the run and (with
--abort-on-bail) sends a terminate control to every live test collector of the
run over its bidirectional connection; each kills its child and exits → EOF (decided
aborted). A job that connects after the bail is terminated on connect (the runner
tracks the run's bail/abort intent). halt wins over retry. The SAME terminate
primitive is the proper teardown for yath abort and owner-disconnect abort: the
runner records the abort intent and messages every live + late-connecting collector
of the run (so it cannot miss a job whose pid had not yet been reported); yath abort
no longer snapshots/INTs pids from the command. A collector that does not comply
within a short grace is hard-killed via kill(-pid) (the handshake pid; a
detached collector setsids so its pid leads its group) — fallback only; the
fire-once ledger still guards the eventual EOF.
Post-pass collector failure flags the suite (§5.4). A collector that fails
after reporting pass keeps its test green (never reopened); when the runner
reaps that collector and sees a non-zero health exit, it reports the error to
harness output and sets a suite-level health-failure flag so yath test/run
exits non-zero even though every test passed. This is the only place the collector
exit code is consulted — post-hoc, suite-level, never a per-test verdict. The runner
reaps both paths now: the no-preload collector is its direct child
(set_proc_exit), and the detached preload collector re-parents to the runner
subreaper (§4a) and is reaped as an unwatched zombie reverse-mapped to its job via
job_pids (_reaped_unwatched_pid → the shared _check_post_pass_health). On a
subreaper-unsupported platform the detached collector re-parents to init, so the
runner never sees its exit and a preload post-pass failure may be lost — accepted.
Collectors self-terminate if the runner dies. Every preload-root, stage, and
job collector is started with watch_parent_pid => <root runner pid> (the runner
conveys its pid to the preload-root via get_preload_list, and the preload-root
passes it down to the stage/job collectors it spawns). Test2::Collector kills the
collector's child and finalizes/exits if the runner disappears while the child runs
— so a runner that dies without signaling (crash / SIGKILL) leaves no orphaned
collectors, stages, preload-root, or test processes. A collector watches the
runner only, never an intermediate stage or the preload-root (a stage reload —
or a preload-root respawn — gets a new pid and must not kill the in-flight test).
The runner-wrap collector is exempt (its child is the runner). Enforced by
agent_scripts/audit-collector-watch-parent.
The runner becomes a child subreaper at the very start of process(), before
any collector is forked (Runner::become_subreaper). The pure-Perl
Test2::Harness2::Util::SubReaper (no XS, no dep) issues the native primitive via
syscall() — Linux prctl(PR_SET_CHILD_SUBREAPER, 1), FreeBSD/DragonFly
procctl(P_PID, $$, PROC_REAP_ACQUIRE) — with a per-(OS, arch) syscall-number
table; an unknown platform or a failed call is an eval-guarded graceful no-op
("unsupported"). Only the runner acquires it — the preload-root/host and forked
stages must not, or a detached collector would stop at them instead of re-parenting
up to the runner.
Consequence for the process tree: a preload test collector double-forks + detaches
(§4 table), so when its short-lived intermediate exits the collector is orphaned and
re-parents to the nearest subreaper ancestor — the runner — on a supported OS,
or to init on an unsupported one. The preload tree then reaps no test
collectors. The collector also setsids into its own session/group, so the runner's
kill(-pid) teardown fallback reaches its whole subtree even on the init-fallback
path. None of this changes the completion decision, which rides EOF + transitions
regardless of who (if anyone) reaps the collector; the subreaper governs only
zombie-ownership and teardown.
All sockets live in the workdir, are unix-domain SOCK_STREAM, and carry the
same wire form: each message is a JSON object, zstd-compressed into one
self-contained frame, via Test2::Collector::Util::Socket (open_unix_listen /
connect_unix / write_frame) + Test2::Collector::Util::Zstd /
...Zstd::FrameBuffer. The harness keeps no copy of this machinery.
| Socket | Server (accepts) | Clients (connect out) | Carries |
|---|---|---|---|
runner.socket |
the runner | test/run/stop/status/ps/abort/resources; the preload-root (handshake: get_preload_list / set_stage_data / resolve_file_stages / preload_warnings); the system-load sampler (one-way system_load reports); every non-runner collector's reporter; each preload stage (its registered service channel) |
control requests (+ replies); transitions from preload-root, stage, sampler & job collectors; the bidirectional runner↔stage channel (see below) |
preload-<stage>.socket (one per preload stage) |
that preload stage | yath spawn connects directly (bypassing the runner, ARCHITECTURE.md §4.8): a spawn request {file, args, env_vars, cwd, abs_path, correlation_id, listen_socket_path} → async {ok=>1} ack |
a Role::Service request/response; the stage async double-forks a detached supervisor that dials back to the command's own listen socket |
command spawn listen socket (File::Temp, 0600) |
the yath spawn command |
the detached spawn supervisor dials back to it | phase 1: SCM_RIGHTS pass of the command's real STDIN/STDOUT/STDERR (Test2::Harness2::Util::FdPass); phase 2: the dedicated control mini-protocol (Test2::Harness2::Util::FdPass::Control) — supervisor pid, forwarded signals, the child's raw wait status, EOF = command died → kill the spawned process group |
command interactive listen socket (File::Temp, 0600; only when --interactive) |
the yath test/yath run command (in a fork that keeps the real STDIN; its path is in $ENV{YATH_INTERACTIVE}) |
each test process dials it (preload: the goto::file filter; no-preload: -MTest2::Harness2::Interactive) |
a single SCM_RIGHTS pass of the command's real STDIN (Test2::Harness2::Util::FdPass::send_fds / connect_stdin), once per sequential test (-j1); no control channel, no framing — the test dup2s the fd onto fd 0 and the connection closes |
sampler.socket |
the system-load sampler | nothing (unused; the sampler dials the runner) | — |
The preload-root dials runner.socket (it does not listen on a socket of its
own; its own output goes to preload-root-events.jsonl.zst and its collector's
Recorder::Socket reporter streams to runner.socket). The runner is the
server on runner.socket. It no longer connects out to the
preload-<stage>.sockets; a stage dials the runner and the runner dispatches back
over that one channel.
Every connection is a Connection object that owns the framing. Five frame kinds,
distinguished by their top-level key:
{ identity => { name => <id>, pid => <pid> } }— sent on open (see lifecycle below). The announcedpidis the peer process's real pid; the receiver stores it ($conn->peer_pid).status/psread each connectedpreload-<stage>peer's pid this way (the scheduler State stores no pid).{ request => { request_id => <uuid>, command => <cmd>, ... } }→ dispatched torequest_handler_<cmd>; the handler's return value is sent back as aresponseechoing therequest_id, or no reply when the handler returnsundef(one-way).{ response => { request_id => <uuid>, ... } }→ matched to the outstanding request byrequest_id. An unmatched response (fire-and-forget reply) is discarded.{ transition => ... }/{ facet_data => { ... } }— a collector transition/event; folded intoRunner::Monitorand forwarded to subscribers.
There is no ordering assumption: an endpoint may send a request and then
receive unrelated messages before the matching response. Correlation is by
request_id (a v7 UUID), never by arrival order.
- Identity exchange. A connection that dials announces its identity
immediately; one that accepts replies with its identity only after seeing the
peer's. Every connection must identify first — including collector reporters,
which send an identity as their first frame via the
Recorder::Socketpreamble. A reporter's identity always carriesno_replyso the runner sends no identity echo back: the echo is noise to a reporter, and writing it back to a collector busy draining its test child (and only opportunistically reading this socket) can stall the runner's single-threaded loop under load. It setsdrain_inputdefensively.no_reply(no echo) is orthogonal toread_control(reads inbound control): a test-job reporter sets both —no_replysuppresses the echo whileread_controlkeeps the connection bidirectional so it still reads the runner's terminate control (line ~40). A non-identity first frame, or no identity before the timeout (~30s), drops the connection. - Bad-frame policy. After identity, 3 consecutive corrupt/invalid frames
(no valid frame between) close the connection; any valid frame resets the count.
A fatal
sysreaderror (ECONNRESET/EBADF) drops it at once.
Role::Service keeps one connection set. Every connection — whether the
service accepted it or dialed it via service_connect_peer — lives in the
same service_select (IO::Select) and service_conns (each fd → its
Connection), and the service reads off all of them. A dialed connection is
not write-only: either end may send requests. The peer registry
(service_peers{<identity>}) backs service_send($identity, $command, %args) and
service_connect_peer's reuse (an existing connection to a peer is reused, never
duplicated). service_subs is the subset flagged as subscribers (pushed
forward_frame deltas).
So runner ↔ stage is one bidirectional channel, not two one-way ones. The
stage dials runner.socket, identifies as preload-<stage>, and that single
connection carries both directions:
stage --[dials runner.socket, identity 'preload-<stage>']--> runner's set
reports UP (stage_ready / stage_down / job_pid / reload)
dispatch DOWN (run_task / stop) — runner service_send's by peer identity over the SAME fd
Commands (test/run/status/...) are full peers too: each connects, identifies,
and issues requests, matching responses by id. A collector's Recorder::Socket
reporter is a separate connection that also identifies first (via its
preamble). A non-test reporter sets no_reply and streams transitions only
(one-way). A test-job reporter is built read_control, so it is bidirectional:
it streams transitions up and reads the runner's inbound terminate control, and
its preamble carries job_id + job_try + run_id for the connection→job map.
Subscribers connect, send a subscribe request, get the
snapshot as its response, then receive forwarded transition frames pushed over
the same fd
(forward_frame over service_subs).
test-job collector ─┐
stage collector ────┤ reporter (Recorder::Socket) streams TRANSITIONS ──► runner.socket
│ (the runner's OWN collector does not report to itself;
│ its transitions go to its own events file)
▼
runner: Runner::Monitor (fold by run; run_uuid == run_id)
│ per-run buckets + a global bucket (runner/stage lifecycle)
│ jobs map (runner-originated job mutations)
│
▼ forward_frame: each subscriber receives ITS run + the global bucket
subscribers: one Runner::Subscriber per command
│ snapshot on connect, then forwarded deltas → a feed-mode Monitor mirror
▼
command-side renderer reads full detail BY PATH ──► the relevant *.jsonl.zst
A subscribe request may carry a run_id (scoped: that run + global) or none
(global: everything — used by watch).
Rendering happens inside the command process (test / run / watch) — there
is no separate renderer/gatherer process.
command (test / run / watch)
└─ Renderer::Base reusable base
├─ transition-state mirror (Monitor, fed by Subscriber)
├─ locates a collector's *.jsonl.zst from transition state, reads BY PATH
│ via Test2::Harness2::JobReader / Test2::Harness2::RunnerReader
├─ rolls up the run verdict → harness_final
└─ render_event fan-out to SINK renderers + the logger:
├─ Test2::Harness2::Renderer::Formatter → Test2::Formatter::* (terminal)
├─ App::Yath2::Renderer::DB (sqlite log / UI store)
├─ App::Yath2::Renderer::Server (DB + live web server)
└─ logger (plain filehandle, JSONL)
Test2::Harness2::Renderer::Driver — subclass of Base used by test/run; per-job
order only (not cross-job). Default: lifecycle
live → whole events file at completion →
job-end last. With --live: every job's events
file is tailed as it appears, so jobs interleave
(each line carries its job_id). Settles a job's
verdict from the events-file terminal.
yath watch — uses Renderer::Base directly as a global
subscriber to render runner/stage output.
JobReader / RunnerReader are by-path readers of a single *.jsonl.zst — they
do not discover or orchestrate.
| Artifact | Created by | Where | Consumed by | Purpose |
|---|---|---|---|---|
runner-events.jsonl.zst |
the runner's non-test collector | workdir | the command renderer (RunnerReader, by path); watch |
runner stdout/stderr/exit as events |
preload-root-events.jsonl.zst |
the preload-root's non-test collector | workdir | the command renderer (RunnerReader); watch |
preload-root + in-process base-stage stdout/stderr/exit as events (also streamed to runner.socket) |
stage-<name>-events.jsonl.zst |
each forked preload stage's non-test collector | workdir | the command renderer (RunnerReader); watch |
stage stdout/stderr/exit as events |
sampler-events.jsonl.zst |
the system-load sampler's non-test collector | workdir | the command renderer (RunnerReader); watch |
sampler stdout/stderr/exit as events (the load data itself rides the system_load request to runner.socket, not this file) |
events.jsonl.zst (per job) |
each test job's test collector | the job's run dir | the command renderer (JobReader, by the path a transition carries) |
the test's full event stream |
aux-<name>-<uuid>.jsonl.zst |
a plugin's shellcall/run_collected aux collector (chunk 17) |
workdir | the command renderer (RunnerReader, by the path a transition announces); watch |
plugin-emitted aux output as events |
.<user>-<host>-<project>-yath-runner.sock (symlink) |
yath start (via App::Yath2::Discovery->publish) |
persist dir (YATH_PERSISTENCE_DIR / --persist-dir, else system temp or cwd) |
run/spawn/stop/which/watch/reload via App::Yath2::Discovery (App::Yath2::Pfile wraps it) |
persistent-runner discovery: a symlink pointing at the workdir's runner.socket; follow it → socket + workdir. Liveness is a socket connect; a dangling/connect-fail link ⇒ runner absent ⇒ the stale link is unlinked |
PID file |
the runner | workdir | discovery resolves it via the symlink (the App::Yath2::Discovery PID-file signal fallback for a wedged runner whose socket is unresponsive); start reads it for its banner; reload HUPs it |
the runner's own pid (survives the exec across a reload-respawn) |
settings.json |
yath start (persistent) |
workdir | yath run (merged into its settings on connect) |
run configuration carried to clients |
The *.jsonl.zst events files are the only files on the IPC/detail path; all
decision and dispatch traffic is on the sockets in §5. (Chunk 17 retired the last
non-events flat file: plugin shellcall/run_collected aux output is now a
collector events stream like any other, reported over runner.socket.)
| Command | Reaches the runner via | Action |
|---|---|---|
test |
spawns the runner, then runner.socket |
submit run + subscribe(run_id) + render. With --interactive: also opens a STDIN fd-pass listen socket (forks to keep the real STDIN), forces -j1/-v/--live, and passes its STDIN to each test in turn |
run |
discover (runner-socket symlink, via Discovery/Pfile) + runner.socket |
submit run(run_id) + subscribe(run_id) + render. --interactive as for test |
spawn |
discover the workdir (runner-socket symlink, via Discovery/Pfile), then connect directly to a preload-<stage>.socket (bypasses the runner) |
require_fdpass('yath spawn') up front; send a spawn request → async {ok=>1} ack; the supervisor dials back to the command's listen socket, the command send_fds its real STDIN/STDOUT/STDERR, then the socket flips to the control protocol (signals + the child's raw wait status). Errors cleanly if no live preload stage exists or IO::FDPass is absent |
watch |
runner.socket |
subscribe (global) + render runner/stage output |
status / ps / abort / resources |
runner.socket |
request → Runner::StatusReport reply |
stop |
runner.socket |
graceful stop request |
reload |
runner-socket symlink → workdir PID → SIGHUP the runner pid |
trigger a preload reload |
which |
runner-socket symlink (via Discovery/Pfile) |
print the discovered persistent runner |