test(e2e): price the layers above the sandbox on the node-join window - #3890
Conversation
Every CPU reading on this failure path has a subject inside the sandbox: a cgroup, a tenant worker's threads, a sandbox node's own /proc/stat. Each of them can say a guest was given its ticks while getting no work done, and none can say what the layers above spent. That is where this suite's own tax is paid: the sandbox nodes' guest time is charged to the runner kernel as user time, and whatever the runner VM loses to the machine hosting it is charged to nobody the sandbox can see. Two readings close that. /proc/stat is not namespaced, so reading it from the sandbox container reads the runner VM's kernel -- the only place steal on that layer appears, which is what separates a slow suite from a machine sharing a core with somebody else. The sandbox VMs are QEMU processes in this container's own PID namespace, so their threads are a local read too, and they say which node spent the kernel's user time and whether the three spent it evenly. Both are taken at the same two points as the KVM counters and for the same reason: the values are cumulative, so a rate needs a pair, and the interval worth measuring is the window the guest is failing to boot in. The thread reading runs the probe the worker capture already uses rather than a copy of it, since a second copy would drift exactly at the failure paths. Each carries the layer it describes in its own legend, because three kernels in this report publish a /proc/stat and two captures read QEMU threads, one layer apart in each direction -- and a reader who takes one for the other compares a hypervisor against its own guest. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughChangesThe node-join diagnostics now capture runner-kernel CPU counters and sandbox QEMU-thread CPU counters before and after the node-join wait. The collectors support bounded reads, partial results, timing metadata, warnings, failure markers, and pairing guidance. Tests cover collection, failures, ordering, budget accounting, and timeout fallback. CPU diagnostic capture
Estimated code review effort: 5 (Critical) | ~90 minutes Sequence Diagram(s)sequenceDiagram
participant NodeJoin as node-join workflow
participant KVM as KVM collector
participant RunnerCPU as runner-kernel CPU collector
participant QEMUCPU as sandbox QEMU-thread collector
participant JoinWait as node-join wait
NodeJoin->>KVM: capture pre-wait sample
NodeJoin->>RunnerCPU: capture pre-wait sample
NodeJoin->>QEMUCPU: capture pre-wait sample
NodeJoin->>JoinWait: wait for node readiness
JoinWait-->>NodeJoin: wait result
NodeJoin->>KVM: capture post-wait sample
NodeJoin->>RunnerCPU: capture post-wait sample
NodeJoin->>QEMUCPU: capture post-wait sample
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
What this PR does
Adds two readings to the tenant node-join path, taken on either side of the same wait the KVM counters already bracket: the runner VM kernel's own
/proc/stat, and the per-thread CPU time of the QEMU processes the sandbox VMs run as.Every CPU reading on this failure path has a subject inside the sandbox: a cgroup, a tenant worker's threads, a sandbox node's own
/proc/stat. Each of them can say a guest was given its ticks while getting no work done, and none can say what the layers above spent. That is where this suite's own tax is paid. The sandbox nodes' guest time is charged to the runner kernel as user time, and whatever the runner VM loses to the machine hosting it is charged to nobody the sandbox can see, so a node reportingsteal 0and a node whose hypervisor was itself waiting look identical from inside./proc/statis not namespaced, so reading it from the sandbox container reads the runner VM's kernel. That is the only place steal on that layer appears, and it is read in one direction only: a steal that climbs proves this runner VM was preempted, while a zero proves nothing, because the column is filled only where the hypervisor exposes a paravirt steal clock and nothing in the capture observes whether this one does. The sandbox nodes a layer down can make the stronger claim, since this repository starts them withaccel=kvm; nobody here starts the runner VM, so for this lane the answer is unknown until somebody reads a non-zero.The sandbox VMs are QEMU processes in the same PID namespace as the shell driving the suite, so their threads are a local read too. What that adds is not "which node is busy" on its own, since
cozy_capture_sandbox_node_cpu_timealready reads each node's own/proc/statby name; it is that split on the join window, since the existing collector samples twice across a twelve-second interval inside the diagnostics block, after the join has already failed. Beyond the window it adds the host-side accounting of each vCPU thread, what the runner actually granted it, which no in-guest counter can report, and QEMU's non-vCPU IO and worker threads, which no in-guest counter sees either. It also names which node each process is, read from thesrv<N>/qemu.pidfiles the bringup already writes: a container pid identifies nothing, and whether the starving guest is the control-plane node or a worker-carrying one is the discriminating question. With no usable pid file the capture stays anonymous and says so, as its KVM sibling does; the pid reads themselves are bounded like every other read here, since a pid file that never yields a byte must not hold the capture.Both are taken at the two points the KVM counters already use, for the same reason: the values are cumulative, so a rate needs a pair, and the interval worth measuring is the window the guest is failing to boot in rather than a few seconds after the deadline has expired. The thread reading runs the probe the tenant worker capture already uses rather than a copy of it, because that probe already answers the two outcomes that matter here and already names the three ways it can come up short, and a second copy would drift from it exactly at those failure paths.
Each capture carries the layer it describes in its own legend. Three kernels in this report publish a
/proc/statand two captures now read QEMU threads, one layer apart in each direction, so a reader who takes one for the other is comparing a hypervisor against its own guest. The runner-kernel legend also carries the trap specific to that layer: nearly all of its user time IS guest time, because running the sandbox VMs is what that kernel does, so adding the two columns double-counts the whole run.Cost and blast radius: two bounded local reads per sample, no cluster access, nothing added to any diagnostics-phase budget window. The failure-path calls sit behind the same reasoning as the KVM reading, which the phase-budget guard in
hack/run-kubernetes-node-join_test.batsnow prices with a cost arm for each; the guards that enumerate collectors (the missing-timeoutwarning and the documented spend order) carry both new names.Tests are in
hack/run-kubernetes-runner-cpu_test.bats, 44 cases, green under both runner forms. They cover the readings themselves, every legend claim a reader depends on, the bounded read and its knobs, the sample-number split, each way a capture can come up empty (a file with nocpurow, a container with no QEMU, a proc mount that yields nothing, a QEMU named and then gone), the routing of a message from a read that succeeded against one that failed, the per-node attribution and each way it can be incomplete, and placement guards of their own: both pairs must bracket the wait on both paths, nothing may be inserted between the three second readings, and each collector must report a shortfall to the job log in its own words rather than only to the report, since one call site is the passing path where the artifact is the one nobody downloads. Two guards exist because a legend can fail without any number being wrong: one asserts that the parse-from-the-last-parenthesis rule the legend states actually works on the lines the capture holds, and one that every bracketed legend line lands as a single line, since an apostrophe inside one of these single-quoted strings shreds it into one word per line while still passing any short grep.No documentation change: the KVM pair this mirrors is not described in
docs/agents/e2e-testing.mdeither, and its documentation lives in the legends the captures carry.Context: #3513
Screenshots
No UI changes.
Downstream repositories
One line of the bringup changes too:
hack/e2e-prepare-cluster.batsnow starts each sandbox QEMU with-name guest=srv<N>,debug-threads=on. QEMU names vCPU threads (CPU N/KVM) only when asked to, so without the flag every thread reports the bare process name and the vCPU/IO split this capture documents would not exist to read on this lane; a test pins the flag to the legend that relies on it.Walked the trigger map in
docs/agents/contributing.mdagainst the diff. Of thehack/triggers it names,hack/e2e-prepare-cluster.bats(ansible-cozystack, talm) is touched: the touched lines are the CI sandbox QEMU flags, which neither downstream repository restates -- both provision real machines, not the e2e sandbox.hack/package.mk,hack/update-crd.shandhack/upload-assets.share untouched. The rest of the change is confined to the Chainsaw kubernetes suites' shared library and its unit tests, which no downstream repository restates.Release note
Summary by CodeRabbit
New Features
Bug Fixes
Tests