ktstr/worker_ready.rs
1//! Shared ready-marker path format for the
2//! `ktstr-jemalloc-alloc-worker` binary and the integration tests
3//! that drive it.
4//!
5//! # Worker → probe ready signaling mechanism (design)
6//!
7//! The worker writes a pid-scoped file after its allocation +
8//! black-box triple completes; the test body polls for that file
9//! before launching the probe. Centralizing the path format here
10//! keeps the worker (`src/bin/jemalloc_alloc_worker.rs`) and the
11//! test (`tests/jemalloc_probe_tests.rs`) in sync — a rename
12//! changes one place, not two.
13//!
14//! Medium: **a file on the shared `/tmp`**. The path is
15//! `/tmp/ktstr-worker-ready-<pid>` where `<pid>` is the worker's
16//! decimal pid (see [`worker_ready_marker_path`] / [`WORKER_READY_MARKER_PREFIX`]).
17//! The worker issues `std::fs::write(path, b"ready\n")` on
18//! ready; the test waits for that file via `wait_for_worker_ready`
19//! in the sibling `worker_ready_wait` module — inotify-event-driven
20//! (`IN_CREATE | IN_MOVED_TO` on the marker's parent directory),
21//! re-checking `Path::exists` on every wake, with a 10 ms sleep
22//! fallback when inotify is unavailable. A test-only env override —
23//! [`WORKER_READY_MARKER_OVERRIDE_ENV`] — replaces the default
24//! pid-scoped path when set and non-empty; production callers
25//! never set it.
26//!
27//! Why a file-on-tmp rather than a pipe / unix socket / vsock /
28//! stdout-token?
29//!
30//! - **Minimal setup before the worker's own allocation path.**
31//! The probe must observe the worker's post-allocation heap
32//! state, not any pre-signaling setup cost. `std::fs::write`
33//! against an existing tmp directory is three syscalls
34//! (`openat` + `write` + `close`) on already-hot kernel
35//! caches; a socket would add `socket` + `connect` +
36//! `sendto` against a daemon that would itself need to be
37//! provisioned by the harness.
38//! - **Shared filesystem namespace without dedicated plumbing.**
39//! Both the worker and the test body run as subprocesses
40//! inside the SAME `#[ktstr_test]` guest VM.
41//! `PayloadRun::spawn` creates the worker via
42//! `std::process::Command`, which inherits the parent's
43//! (guest-side) filesystem namespace, so the two processes
44//! see the same guest-VM tmpfs `/tmp`. No host involvement,
45//! no bind-mount, no guest↔host bridge. A socket path would
46//! still require a dedicated in-VM dispatcher; vsock would
47//! require a cid allocation.
48//! - **No process-of-write hard dependency.** A pipe close or
49//! EOF on the worker's stdout would also signal readiness,
50//! but any crashing worker would look the same — the file
51//! approach surfaces "worker reached the signaling point"
52//! distinctly from "worker died before signaling".
53//!
54//! # Dual-compilation constraint — MUST STAY STD-ONLY
55//!
56//! This source file is compiled TWICE by the same `cargo build`:
57//! once as `ktstr::worker_ready` (a lib-crate module) and once as the
58//! worker bin's own `mod worker_ready` via
59//! `#[path = "../worker_ready.rs"]` (see
60//! `src/bin/jemalloc_alloc_worker.rs`). The `#[path]` include is
61//! deliberate: linking the entire ktstr library into a worker
62//! process would pull thousands of unused symbols and perturb the
63//! probe's cross-process timing.
64//!
65//! Consequences for this file:
66//! - **No `crate::…` paths**, no `use crate::…` statements.
67//! `crate` resolves to two different crates (ktstr vs.
68//! the bin) on the two compilation paths; anything that names the
69//! other crate's types breaks one of the two builds. The nested
70//! `#[cfg(test)] mod tests { … }` block at the bottom of this file
71//! uses `super::` to reach items defined here — `super::` from a
72//! child `mod tests` resolves to this file's own items, which exist
73//! identically under both compilation paths, so the tests do not
74//! divide lib vs. bin. Those tests pin the path format (the prefix
75//! literal, decimal-pid formatting, and the override-env / stderr /
76//! stdout prefixes). `tests/jemalloc_alloc_worker_exit_codes.rs`
77//! and `tests/jemalloc_probe_signals_test.rs` merely consume the
78//! same items through the `ktstr::worker_ready::…` public surface;
79//! they do not re-pin the path-format literal.
80//! - **No ktstr-library types or modules.** Only `std` items,
81//! language primitives, and `core` types are safe. Anything that
82//! depends on `PayloadHandle`, scenario `Ctx`, `anyhow`, or any
83//! other lib-only item must live in
84//! [`ktstr::worker_ready_wait`] (lib-only) — not here.
85//! - **No external crate imports that only the lib or only the bin
86//! has.** Adding a non-std dependency requires a matching `Cargo.toml`
87//! stanza for both the lib and the bin; otherwise one build path
88//! fails to resolve the crate.
89//! - **No `#[cfg(feature = "…")]` that differs across the two
90//! crates.** Feature gates evaluate per-crate, so a gate that's
91//! satisfied for the lib but not the bin (or vice versa) will
92//! silently diverge the two compiled copies.
93//!
94//! The `wait_for_worker_ready` helper lives in the sibling
95//! [`ktstr::worker_ready_wait`] module because it needs
96//! `PayloadHandle` and therefore depends on the rest of the
97//! library.
98//!
99//! # In-VM invariants
100//!
101//! The ready-marker scheme relies on two properties that the
102//! `#[ktstr_test]` VM environment supplies:
103//!
104//! - **Shared `/tmp` between worker and reader.** Both the
105//! worker and the test body run as subprocesses inside the
106//! same guest VM, spawned via `PayloadRun::spawn` →
107//! `std::process::Command`. The child inherits the parent's
108//! guest-side filesystem namespace, so both processes see
109//! the same guest-VM tmpfs `/tmp`. The marker is NOT
110//! transported over a socket / pipe: if a future refactor
111//! puts the worker in a distinct filesystem namespace
112//! (e.g. via `unshare --mount` or a separate VM), the poll
113//! will always time out and the ready-signal must move to a
114//! different medium (unix socket, `vsock`, or a stdout-token
115//! parse).
116//! - **`PayloadHandle::pid() == std::process::id()` inside the guest.**
117//! The test body reads `PayloadHandle::pid()` to learn the
118//! worker's pid, which is the same pid the worker observes
119//! via `getpid()` / `std::process::id()` inside the VM —
120//! single-namespace because the worker runs without a
121//! separate pid-namespace. Consumers that add a pid-namespace
122//! or run the worker under something like `unshare --fork
123//! --pid` must also translate the pid before constructing
124//! the path, or the reader polls a path that the writer
125//! never materialized.
126//!
127//! Both properties are invariants the `ktstr-jemalloc-alloc-worker`
128//! + `jemalloc_probe_tests.rs` pair depends on; breaking either
129//! without updating this module's scheme produces silent poll
130//! timeouts rather than loud errors.
131
132/// Prefix for the pid-scoped ready-marker path. The final segment is
133/// the worker's pid rendered as a decimal ASCII integer.
134///
135/// Exported as a `pub const` for symmetry with the other items in
136/// this module ([`WORKER_READY_MARKER_OVERRIDE_ENV`],
137/// [`worker_ready_marker_path`], [`WORKER_STDERR_PREFIX`]) — every
138/// symbol that represents a piece of the worker-ready / worker-
139/// stderr wire contract is `pub` so host-side integration tests in
140/// `tests/` can assert on the exact literal the worker and the
141/// probe depend on, without the test file having to duplicate the
142/// string. Downstream callers (the worker binary's ready-path
143/// write, the host-side poll in `wait_for_worker_ready`) normally
144/// route through [`worker_ready_marker_path`] rather than
145/// concatenating this prefix themselves; the prefix is exposed for
146/// assertion use, not as the preferred construction path.
147pub const WORKER_READY_MARKER_PREFIX: &str = "/tmp/ktstr-worker-ready-";
148
149/// Name of the test-only env var that overrides the pid-scoped
150/// default path. When set and non-empty, the worker writes the
151/// ready marker at the override path instead of
152/// [`worker_ready_marker_path(pid)`](worker_ready_marker_path);
153/// when unset (or empty) the default pid-scoped path applies.
154///
155/// Exported as a `pub const` so both the worker binary and the
156/// integration tests that drive it share a single source of truth
157/// — eliminating the string-literal drift window where the worker
158/// and a test disagree on the env-var name and the override
159/// silently fails to take effect.
160pub const WORKER_READY_MARKER_OVERRIDE_ENV: &str = "KTSTR_WORKER_READY_MARKER_OVERRIDE";
161
162/// Construct the ready-marker path for a worker with the given pid.
163///
164/// The worker uses [`std::process::id()`] (`u32`) as the pid source
165/// inside its own process. The host-side test reads the worker's
166/// pid via `PayloadHandle::pid()`, which returns `Option<u32>` —
167/// `Some(pid)` once the child has been spawned and `None` before
168/// (or after a kill that tore the child down). Callers must
169/// unwrap (or propagate) the `Option` at the call site; this
170/// helper takes a bare `u32` so the unwrap decision stays visible
171/// to the caller instead of being swallowed inside the formatter.
172/// The `u32` parameter matches both the worker's
173/// `std::process::id()` return type and the unwrapped payload of
174/// `PayloadHandle::pid()` without per-caller casts.
175pub fn worker_ready_marker_path(pid: u32) -> String {
176 format!("{WORKER_READY_MARKER_PREFIX}{pid}")
177}
178
179/// Stderr line prefix the `ktstr-jemalloc-alloc-worker` binary
180/// prepends to every fail-fast diagnostic it emits (missing argv,
181/// bytes=0, thread self-check, procfs unreadable, ready-marker
182/// write fail). Exported as a `pub const` so host-side integration
183/// tests can assert against the binary's emitted literal without
184/// duplicating it — a rename or a typo on either side shows up
185/// here in one place instead of silently desynchronizing.
186///
187/// The trailing space that separates the prefix from the message
188/// body is NOT part of this constant: call sites write
189/// `{WORKER_STDERR_PREFIX} …` so the space remains a formatting
190/// concern, and test-side substring checks can match on the
191/// prefix alone regardless of the specific separator the worker
192/// chooses.
193pub const WORKER_STDERR_PREFIX: &str = "jemalloc-alloc-worker:";
194
195/// Stdout "ready" breadcrumb the `ktstr-jemalloc-alloc-worker`
196/// binary prints once, immediately before entering its terminal
197/// loop — the default-mode `sleep(3600s)` park loop or the
198/// `--churn` spawn+join loop — after its allocation + `black_box`
199/// triple has materialised.
200/// The full emitted line is
201/// `{WORKER_STDOUT_READY_PREFIX} pid={pid} bytes={bytes}`; this
202/// const carries only the prefix so host-side consumers that want
203/// to grep the worker's captured stdout (or that fold worker
204/// stdout into a larger test log) can match against a single
205/// authoritative literal.
206///
207/// Exported as `pub const` for the same reason as
208/// [`WORKER_STDERR_PREFIX`]: a rename or a typo on either side
209/// shows up in one place instead of silently desynchronising the
210/// worker and any test-side assertion. The breadcrumb is
211/// currently not parsed by any automated consumer — readiness is
212/// signalled via the marker file ([`worker_ready_marker_path`]),
213/// NOT via stdout — but pinning the literal here means a future
214/// test that wants to correlate "worker log says ready" with
215/// "marker file appeared" has a stable hook.
216///
217/// The trailing space separating the prefix from the
218/// `pid=` / `bytes=` tail is NOT part of the constant, matching
219/// the [`WORKER_STDERR_PREFIX`] convention.
220pub const WORKER_STDOUT_READY_PREFIX: &str = "jemalloc-alloc-worker ready";
221
222#[cfg(test)]
223mod tests {
224 // `super::` only — this file is dual-compiled (lib + bin) per the
225 // module-doc dual-compilation constraint. `crate::` resolves to two
226 // different crates; `super::` reaches this file's own items
227 // identically under both compilation paths.
228 use super::*;
229
230 /// `worker_ready_marker_path` formats the pid as a decimal ASCII
231 /// suffix on the canonical prefix. Pins the wire-format contract
232 /// the worker (in `src/bin/jemalloc_alloc_worker.rs`) and the
233 /// poller ([`crate::worker_ready_wait::wait_for_worker_ready`])
234 /// agree on. A rename of the prefix or a switch to hex/zero-pad
235 /// pid formatting would surface here.
236 #[test]
237 fn worker_ready_marker_path_decimal_pid_suffix() {
238 assert_eq!(worker_ready_marker_path(0), "/tmp/ktstr-worker-ready-0",);
239 assert_eq!(worker_ready_marker_path(1), "/tmp/ktstr-worker-ready-1",);
240 assert_eq!(
241 worker_ready_marker_path(12345),
242 "/tmp/ktstr-worker-ready-12345",
243 );
244 assert_eq!(
245 worker_ready_marker_path(u32::MAX),
246 format!("/tmp/ktstr-worker-ready-{}", u32::MAX),
247 );
248 }
249
250 /// The path produced by `worker_ready_marker_path` always begins
251 /// with `WORKER_READY_MARKER_PREFIX`. Test-side assertions that
252 /// match against the prefix alone (rather than re-deriving the
253 /// full path) stay correct only as long as this invariant holds.
254 #[test]
255 fn worker_ready_marker_path_starts_with_prefix() {
256 for pid in [0, 1, 100, 65535, u32::MAX] {
257 let path = worker_ready_marker_path(pid);
258 assert!(
259 path.starts_with(WORKER_READY_MARKER_PREFIX),
260 "path {path:?} must start with prefix {:?}",
261 WORKER_READY_MARKER_PREFIX,
262 );
263 }
264 }
265
266 /// Pin the literal prefix value. The worker binary writes a file
267 /// at this path; integration tests grep for the prefix; a
268 /// rename-without-coordinated-update would silently desync the
269 /// two halves. Pin the literal so the rename surfaces as a test
270 /// failure, not as a poll timeout.
271 #[test]
272 fn worker_ready_marker_prefix_literal() {
273 assert_eq!(WORKER_READY_MARKER_PREFIX, "/tmp/ktstr-worker-ready-",);
274 }
275
276 /// Pin the literal env-var name. The worker reads this env var
277 /// to optionally override the default pid-scoped path; tests in
278 /// `tests/jemalloc_alloc_worker_exit_codes.rs` and
279 /// `tests/ctprof_capture_jemalloc_wiring.rs` reference the same
280 /// const via the `pub` re-export. A typo on either side would
281 /// silently break the override path.
282 #[test]
283 fn worker_ready_marker_override_env_literal() {
284 assert_eq!(
285 WORKER_READY_MARKER_OVERRIDE_ENV,
286 "KTSTR_WORKER_READY_MARKER_OVERRIDE",
287 );
288 }
289
290 /// Pin the worker's stderr-line prefix and stdout-ready prefix
291 /// literals. Both are exposed for host-side test grepping; a
292 /// rename without updating callers would silently lose all
293 /// matches.
294 #[test]
295 fn worker_log_prefix_literals() {
296 assert_eq!(WORKER_STDERR_PREFIX, "jemalloc-alloc-worker:");
297 assert_eq!(WORKER_STDOUT_READY_PREFIX, "jemalloc-alloc-worker ready",);
298 }
299}