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}