Skip to content

test(e2e): measure tenant worker CPU as a rate, and the sandbox nodes under it - #3836

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
feat/e2e-nodejoin-batch2-collectors
Aug 15, 2026
Merged

test(e2e): measure tenant worker CPU as a rate, and the sandbox nodes under it#3836
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
feat/e2e-nodejoin-batch2-collectors

Conversation

@lexfrei

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

Copy link
Copy Markdown
Contributor

What this PR does

Two instruments for the tenant worker that misses the node-Ready deadline. They are instruments, not a fix: the failure they instrument is #3513, and this PR does not close it. Neither changes what the suites assert.

The worker CPU capture now reads container_cpu_usage_seconds_total beside the CFS counters it already read, and it is read twice a fixed interval apart instead of once. The throttled counters say a container met its ceiling; they cannot say how much CPU it got, and the two failures behind a stalled worker are exactly those, a guest held at a one-core ceiling and a guest whose vCPU thread is rarely scheduled at all. Both leave throttled periods behind. Every counter involved is cumulative since the container started, so a single reading divides out to an average over the whole uptime and says nothing about the window the deadline covered, which matters here because the profile under investigation is a guest that freezes for tens of seconds and then runs flat out: the average comes out low, the instantaneous figure comes out at the cap, and only the second distinguishes it from a guest that is simply capped.

The second instrument is a new subject. The worker captures describe a cgroup, so they are blind to the layer under the sandbox, and a guest that is runnable and never scheduled looks from the cgroup like a guest that asked for nothing. Whether the sandbox node itself was given its turn is recorded only in the steal column of its own /proc/stat. No kubelet endpoint carries it, and crust-gather cannot reach it because PodSecurity refuses its node-shell Pod in this cluster, so the capture goes over the Talos API, which is already how the report reads those nodes for dmesg. It is captured verbatim with its column legend beside it rather than reduced to a percentage: /proc/stat has no header, steal and guest sit next to each other, and guest is large on a node running VMs and already counted inside user, so reading one as the other turns a fraction of a percent into twenty-odd.

Both subjects are read on either side of one shared wait, so the pair pays it once. That wait is guarded like every other external in the block: it is the one call there outside a || true, and under set -eu a missing sleep would exit 127 and take the collectors after it, two of which the phase's own missing-timeout warning promises keep collecting. Without it the two readings are taken back to back and the block says so, which is honest here because the interval is read off the stamps rather than off the knob. The default is 12 seconds and that is the whole wall-clock cost this adds to a failing run. The wait is not the interval either subject's counters span, though: each pass walks its own nodes and the other subject's pass falls between a subject's two readings, and on the run these diagnostics exist for those passes are the slow part rather than the wait. Each capture therefore stamps both ends of its read, so the real interval is subtracted off the pair instead of assumed from the knob, and the width of those brackets is the uncertainty. This matters more on the sandbox side than on the worker side, since a cAdvisor row carries its own sample time and a /proc/stat row carries none. That wait is a measurement interval rather than a stand-in for a condition nobody wrote a wait for, so docs/agents/e2e-testing.md carves it out by name instead of leaving a reader to decide whether the TODO(e2e-replace-fixed-timeouts): marker applies to a gap between two readings of a cumulative counter.

The pair is gated below the serial console rather than above it, and its ceiling rather than its worth is the reason. Reading two collectors twice is sixteen bounded reads plus the interval, which at the read bound and the three-node cap is most of the phase budget, and admission gates only when a collector may start. Above the console, a run that hit those bounds would leave the console never started rather than merely cut short, and the console is the only capture that survives a worker which never reached apid, which is the shape this failure usually takes. That overrun is inside the term the budget derivation already allows for one collector admitted a moment before the deadline, and a guard in hack/run-kubernetes-node-join_test.bats holds the pair's ceiling against it. Two more guards there hold what may sit ahead of the console: nothing read more than once, and nothing whose ceiling could spend the budget on its own.

Failure marking follows the existing collectors rather than inventing a shape: no collector here may leave an empty directory, because an empty directory reads as a healthy node, so a missing binary, a missing talosconfig, a listing that never answered, a node with no address and a read that was cut off each land as a named note or a COLLECTION-FAILED.txt.

The phase budget is unchanged at 420s and the derivation guard still holds.

Covered by hack/run-kubernetes-sandbox-cpu-time_test.bats (new) plus additions to the cpu-throttle, node-join and network-counters suites. Each collector was checked against a deliberate break of its own guard: dropping the usage family, ignoring the sample number, removing the interval, accepting a zero interval, swallowing the missing-talosconfig marker, reading the wrong /proc file, taking a dual-stack node's second address for a node, counting addresses instead of nodes in the truncation marker, stamping only one end of a read, letting the stamp claim counters were sampled on an arm that read nothing, telling a capture with no counters to subtract a sibling, leaving the sampling wait unguarded, and starving the budget guard of its input. Every one is caught by a named test.

Screenshots

Not applicable, no UI change.

Downstream repositories

The diff touches hack/e2e-chainsaw/_lib/run-kubernetes.sh, two chainsaw-test.yaml files, four BATS files and one agent doc. Walking the trigger map: nothing under hack/ is moved or renamed, hack/package.mk and hack/common-envs.mk are untouched, no make target changes what it does, no package is added or removed, no schema, default, enum, CRD or namespace moves. The one entry that mentions this area is the ccp rule about moving or renaming things under hack/, and no file is moved or renamed here.

Release note

test(e2e): the node-join failure diagnostics now read the tenant worker CPU counters twice a fixed interval apart, so consumed CPU and throttling are both available as a rate over the same window instead of as an average over the container's uptime, and they add the sandbox node's own /proc/stat over the Talos API, which is the only place the steal time under the sandbox is recorded. Each reading carries the time it was taken, so the interval a rate divides by comes from the captures rather than from the configured wait.

Summary by CodeRabbit

  • New Features

    • Enhanced Kubernetes diagnostics with paired, timestamped samples for worker CPU usage and throttling.
    • Added sandbox-node CPU steal-time diagnostics, including support for partial, unavailable, or timed-out captures.
    • Improved diagnostic reports with clearer capture status, rate-calculation context, and sample-specific artifacts.
    • Updated diagnostic sequencing and time budgets to accommodate the expanded metrics collection.
  • Documentation

    • Clarified when measurement intervals are appropriate and updated the review checklist accordingly.

… under it

Two instruments for the node-join failure the kubernetes-* suites keep
hitting. Neither changes what the suites assert.

The worker CPU capture now reads container_cpu_usage_seconds_total
beside the CFS counters, and reads them twice a fixed interval apart
instead of once. The throttled counters say a container met its ceiling
and cannot say how much CPU it got, which are the two candidate
explanations for a stalled worker: a guest held at a one-core ceiling,
and a guest whose vCPU thread is rarely scheduled at all. Both leave
throttled periods behind. Every counter involved accumulates from
container start, so a single reading is an average over the whole uptime
and no rate at all, and the profile under investigation is a guest that
freezes for tens of seconds and then runs flat out: the average comes
out low, the instantaneous figure comes out at the cap, and only the
second separates it from a guest that is simply capped.

The sandbox nodes are a new subject. The worker captures describe a
cgroup, so a guest that is runnable and never scheduled looks from there
like a guest that asked for nothing. Whether the sandbox node itself was
given its turn is recorded only in the steal column of its own
/proc/stat: no kubelet endpoint carries it, and crust-gather cannot
reach it because PodSecurity refuses its node-shell Pod here, so the
read goes over the Talos API, which is already how the report reaches
those nodes for dmesg. The rows are captured verbatim with their column
legend beside them rather than reduced to a percentage, because
/proc/stat has no header, steal and guest sit next to each other, and
guest is large on a node running VMs and already counted inside user.

Both subjects are read on either side of one shared wait, so the pair
pays it once. The wait is not the interval either subject's counters
span, though: each pass walks its own nodes and the other subject's pass
falls between a subject's two readings, and on the run this exists for
those passes are the slow part. Each capture therefore stamps both ends
of its read, so a rate comes from subtracting stamps rather than from
assuming the knob, and the width of those brackets is the uncertainty.
That wait is a measurement interval rather than a stand-in for a
condition nobody wrote a wait for, so the e2e conventions carve it out
by name instead of marking it as a fixed timeout to be replaced.

The pair is gated below the serial console rather than above it. Its
ceiling is sixteen bounded reads plus the interval, which is most of the
phase budget, and admission gates only when a collector may start, so
above the console a run that hit those bounds would leave the console
never started rather than merely cut short. The console is the only
capture that survives a worker which never reached apid, which is the
shape this failure usually takes. That overrun is inside the term the
budget derivation already allows for one late admission, and a guard
holds the pair's ceiling against it.

No collector here may leave an empty directory, since an empty directory
reads as a healthy node: a missing binary, a missing talosconfig, a
listing that never answered, a node with no address and a read that was
cut off each land as a named note or a COLLECTION-FAILED.txt.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <3811295@gmail.com>
@github-actions github-actions Bot added area/testing Issues or PRs related to testing (e2e, bats, unit tests) size/XXL This PR changes 1000+ lines, ignoring generated files labels Aug 15, 2026
@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 0cf82eb into main Aug 15, 2026
8 of 10 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the feat/e2e-nodejoin-batch2-collectors branch August 15, 2026 07:53
@coderabbitai

coderabbitai Bot commented Aug 15, 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: a9a73854-669e-4090-9c0f-9b9da8e527f0

📥 Commits

Reviewing files that changed from the base of the PR and between 3dd142a and 527d4ea.

📒 Files selected for processing (8)
  • docs/agents/e2e-testing.md
  • hack/e2e-chainsaw/_lib/run-kubernetes.sh
  • hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml
  • hack/e2e-chainsaw/kubernetes-previous/chainsaw-test.yaml
  • hack/run-kubernetes-cpu-throttle_test.bats
  • hack/run-kubernetes-network-counters_test.bats
  • hack/run-kubernetes-node-join_test.bats
  • hack/run-kubernetes-sandbox-cpu-time_test.bats

📝 Walkthrough

Walkthrough

The diagnostic phase now captures paired worker CPU/throttling and sandbox-node CPU samples. It records timestamps, bounds reads, handles incomplete results, updates collector ordering and budgets, and adds extensive Bats coverage.

Changes

Sampled CPU diagnostic flow

Layer / File(s) Summary
Collector implementation
hack/e2e-chainsaw/_lib/run-kubernetes.sh
Worker cAdvisor captures now include CPU usage, throttling, timestamps, and sample-scoped reports. A sandbox-node collector reads Talos /proc/stat with bounded execution and records failures, partial results, and timestamps.
Sampling and diagnostic orchestration
hack/e2e-chainsaw/_lib/run-kubernetes.sh, hack/e2e-chainsaw/kubernetes-*/chainsaw-test.yaml, docs/agents/e2e-testing.md
The diagnostic phase coordinates two samples, validates the sampling interval, handles missing sleep, updates phase budgets and ordering, and documents the measurement-interval exception.
Worker and network capture validation
hack/run-kubernetes-cpu-throttle_test.bats, hack/run-kubernetes-network-counters_test.bats
Tests cover sample-scoped artifacts, CPU usage, timestamps, incomplete reads, filtering, ordering, paired-capture guidance, and single network captures.
Sandbox collector and integration validation
hack/run-kubernetes-node-join_test.bats, hack/run-kubernetes-sandbox-cpu-time_test.bats
Tests cover Talos configuration, node discovery, bounded reads, failures, partial results, truncation, timeout behavior, phase budgets, ordering, and sample directories.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DiagnosticPhase
  participant WorkerCPUCollector
  participant SandboxCPUCollector
  participant cAdvisor
  participant Talosctl

  DiagnosticPhase->>WorkerCPUCollector: Capture sample 1
  WorkerCPUCollector->>cAdvisor: Read worker CPU metrics
  DiagnosticPhase->>SandboxCPUCollector: Capture sample 1
  SandboxCPUCollector->>Talosctl: Read sandbox /proc/stat
  DiagnosticPhase->>DiagnosticPhase: Wait for sampling interval
  DiagnosticPhase->>WorkerCPUCollector: Capture sample 2
  DiagnosticPhase->>SandboxCPUCollector: Capture sample 2
Loading

Possibly related PRs

Suggested labels: debug

Suggested reviewers: myasnikovdaniil, ivanhunters

✨ 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-batch2-collectors

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.

Aleksei Sviridkin (lexfrei) added a commit that referenced this pull request Aug 15, 2026
## What this PR does

One line in the kubernetes e2e suite values: tenant workers move from
`u1.medium` to `u1.large`, one vCPU to two.

The reason is measured, in two independent ways on
#3513. A single-scrape reading on a quiet branch run
and the two-scrape instrument from #3836 on a loaded run with six
parallel e2e workflows agree: the worker's compute container sits at 93
to 98 percent of its one-core ceiling for the whole node-Ready budget,
the sandbox nodes under it show ~0.05 percent steal, and raising the CPU
request changes nothing because the workers never lose a scheduling
fight. The binding constraint is capacity: one vCPU has to boot Talos
and unpack the kubelet image, and its quota also covers the QEMU
emulator and IO threads.

This is an experiment as much as a fix: the suites run the same
assertions, and the next waves of runs under parallel load measure
whether the join flake documented in #3513 disappears
with the ceiling doubled. The diagnostics landed in #3804 and #3836 stay
aboard, so the same instrument reports the new profile.

Memory math on the sandbox: two suites with two workers each move from
4Gi to 8Gi guests, and the virt-launcher pods spread across three 24Gi
nodes, so the placement still fits.

### Screenshots

Not applicable, no UI change.

### Downstream repositories

The diff is one value inside `hack/e2e-chainsaw/_lib/run-kubernetes.sh`.
Nothing under `hack/` is moved or renamed, no make target changes
behaviour, no package, schema, default or CRD changes. Nothing in the
trigger map is touched.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
test(e2e): tenant workers in the kubernetes e2e suites run with two vCPUs, since measurements showed the single-vCPU worker saturating its CPU ceiling for the entire node-join budget
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated the tenant worker node configuration to use a larger instance
type, improving available capacity for end-to-end Kubernetes testing.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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