Skip to content

test(e2e): price the layers above the sandbox on the node-join window - #3890

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
feat/e2e-nodejoin-l1-cpu
Aug 18, 2026
Merged

test(e2e): price the layers above the sandbox on the node-join window#3890
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
feat/e2e-nodejoin-l1-cpu

Conversation

@lexfrei

@lexfrei Aleksei Sviridkin (lexfrei) commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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 reporting steal 0 and a node whose hypervisor was itself waiting look identical from inside.

/proc/stat is 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 with accel=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_time already reads each node's own /proc/stat by 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 the srv<N>/qemu.pid files 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/stat and 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.bats now prices with a cost arm for each; the guards that enumerate collectors (the missing-timeout warning 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 no cpu row, 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.md either, 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.bats now 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.md against the diff. Of the hack/ 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.sh and hack/upload-assets.sh are 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

test(e2e): a tenant node-join failure now records the runner kernel's own CPU time and the sandbox VMs' per-thread QEMU CPU time on either side of the join wait, so steal on the layer above the sandbox and an uneven split across the three nodes are both visible in cozyreport.tgz

Summary by CodeRabbit

  • New Features

    • Added runner and sandbox CPU diagnostics to node-join troubleshooting captures.
    • Captures now include timing details, layer-specific context, partial-result handling, and explicit failure indicators.
    • Enabled identifiable virtual CPU thread names for improved diagnostic reporting.
  • Bug Fixes

    • Improved warning visibility when CPU diagnostic samples are unavailable or time out.
    • Added safer handling for incomplete reads, missing data, and shell errors.
  • Tests

    • Expanded coverage for CPU capture behavior, timeout handling, ordering, and failure scenarios.

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>
@github-actions github-actions Bot added size/XXL This PR changes 1000+ lines, ignoring generated files area/testing Issues or PRs related to testing (e2e, bats, unit tests) labels Aug 18, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 974c280 into main Aug 18, 2026
9 of 11 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the feat/e2e-nodejoin-l1-cpu branch August 18, 2026 04:28
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 783a7afe-70ae-4c31-97c4-03c2c3e60a1a

📥 Commits

Reviewing files that changed from the base of the PR and between c5e175a and b1b8cbe.

📒 Files selected for processing (6)
  • hack/e2e-chainsaw/_lib/run-kubernetes.sh
  • hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml
  • hack/e2e-chainsaw/kubernetes-previous/chainsaw-test.yaml
  • hack/e2e-prepare-cluster.bats
  • hack/run-kubernetes-node-join_test.bats
  • hack/run-kubernetes-runner-cpu_test.bats

📝 Walkthrough

Walkthrough

Changes

The 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

Layer / File(s) Summary
CPU capture collectors
hack/e2e-chainsaw/_lib/run-kubernetes.sh, hack/e2e-prepare-cluster.bats, hack/run-kubernetes-runner-cpu_test.bats
The new collectors read runner /proc/stat and sandbox QEMU thread data. They classify complete, partial, empty, and unavailable results. They record timing, legends, warnings, and failure artifacts. QEMU launches with identifiable thread names.
Node-join sampling and budget wiring
hack/e2e-chainsaw/_lib/run-kubernetes.sh, hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml, hack/e2e-chainsaw/kubernetes-previous/chainsaw-test.yaml
Both collectors run around the node-join wait. Their bounded reads count against the diagnostic budget. Missing readings produce warnings. Configuration comments document their placement and budget handling.
Node-join workflow validation
hack/run-kubernetes-node-join_test.bats, hack/run-kubernetes-runner-cpu_test.bats
Tests stub both collectors and validate timeout warnings, budget accounting, sampling order, KVM adjacency, pairing, and shell-error-safe paths.

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
Loading
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2e-nodejoin-l1-cpu

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/testing Issues or PRs related to testing (e2e, bats, unit tests) size/XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant