Skip to content

fix(e2e): disable guest fsync on ephemeral CI VM disks - #3455

Merged
Andrei Kvapil (kvaps) merged 2 commits into
mainfrom
fix/e2e-qemu-drive-cache-unsafe
Jul 29, 2026
Merged

fix(e2e): disable guest fsync on ephemeral CI VM disks#3455
Andrei Kvapil (kvaps) merged 2 commits into
mainfrom
fix/e2e-qemu-drive-cache-unsafe

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented Jul 27, 2026

Copy link
Copy Markdown
Member

What this PR does

The e2e QEMU guests are launched in hack/e2e-prepare-cluster.bats with three -drive … if=virtio,format=raw lines that carry no cache= option, so QEMU falls back to its cache=writeback default, which honors every guest fsync against the host-backed raw disk files.

The management-cluster Talos etcd — which holds every operator's leader-election lease — fsyncs on each Raft commit. During the VM-heavy suites, containerd is simultaneously unpacking dozens of images and CDI is importing multi-GB VM disks onto the same host-backed raw files. The resulting fsync storm serializes etcd's writes into multi-second stalls, so the management apiserver returns context deadline exceeded on lease PUTs, and several operators (capk, etcd-operator, fdb-operator, cilium-operator, kamaji) lose leadership at the same moment. Whichever chainsaw suite is mid-flight then fails — a different one each run (vmcluster, kubernetes-latest, vmdisk), which is the fingerprint of shared-resource contention rather than a bug in any single suite.

This adds cache=unsafe to each guest drive. That makes guest flushes no-ops against the host page cache, which removes the fsync-gated etcd stalls at the root of the flakiness. It is safe here precisely because these CI VMs are ephemeral and destroyed at the end of every run — a crash before writeback loses only throwaway state — and it must never be used for real data. The change is minimal and uniform across all three guest drives; no non-guest drive is touched (there are none in this file).

Runner sizing alone (#3251) did not resolve this, because more CPU and memory do not change the I/O path: the guest fsyncs still propagate through cache=writeback to the host, and etcd still stalls behind the same fsync storm. This addresses the I/O path directly.

git log -S 'cache=' on this file is empty, so a cache mode has never previously been set here.

Screenshots

Not applicable — no UI changes.

Downstream repositories

I walked the trigger map in docs/agents/contributing.md against the actual diff. The only entries that reference hack/e2e-prepare-cluster.bats (ansible-cozystack and ccp) are scoped to node prerequisites — kernel modules, containerd settings, the LVM global_filter, and the cluster domain. This change touches none of those: it adds a QEMU host-side hypervisor flag (cache=unsafe) to the CI VM -drive options, which is invisible to the guest OS and to any real node. No downstream repository is affected.

Release note

fix(e2e): boot e2e QEMU guests with cache=unsafe so ephemeral CI VM fsyncs no longer stall the management-cluster etcd, removing the cross-operator leader-election flakiness that failed a different chainsaw suite each run

This PR was prepared with the assistance of an AI agent (Claude). A human maintainer reviewed the diff and the reasoning before submission and remains responsible for the change.

Summary by CodeRabbit

  • Tests
    • Updated end-to-end virtual machine startup testing to use optimized QEMU disk caching settings for improved reliability and performance.
    • Improved end-to-end Kubernetes cluster readiness checks under load by extending the deadline for reaching “at least 2 nodes Ready,” and aligned related messaging and failure diagnostics accordingly.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions github-actions Bot added area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug size/XS This PR changes 0-9 lines, ignoring generated files labels Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e9297696-ad74-4161-8275-a2fcb4cce4af

📥 Commits

Reviewing files that changed from the base of the PR and between 18c78e1 and 330acfe.

📒 Files selected for processing (2)
  • hack/e2e-chainsaw/_lib/run-kubernetes.sh
  • hack/e2e-prepare-cluster.bats

📝 Walkthrough

Walkthrough

E2E helpers now use unsafe QEMU disk caching and allow up to 18 minutes for at least two tenant nodes to become Ready, with updated explanatory comments and failure diagnostics.

Changes

E2E infrastructure adjustments

Layer / File(s) Summary
Configure VM disk caching
hack/e2e-prepare-cluster.bats
Adds cache=unsafe to the system, seed, and data disk options used when booting each VM.
Extend tenant readiness deadline
hack/e2e-chainsaw/_lib/run-kubernetes.sh
Extends the unified tenant node readiness timeout from 12 to 18 minutes and updates related comments and failure messaging.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: androndo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: making ephemeral CI VM disks unsafe-cache to avoid guest fsync impact.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 fix/e2e-qemu-drive-cache-unsafe

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.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@dosubot dosubot Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation kind/flake Categorizes issue or PR as related to a flaky test labels Jul 27, 2026
@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files and removed size/XS This PR changes 0-9 lines, ignoring generated files labels Jul 27, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/e2e-chainsaw/_lib/run-kubernetes.sh`:
- Around line 444-467: The fixed 18m node-readiness wait can exceed the
enclosing 40m Chainsaw step after earlier waits consume most of the budget.
Update the node-join polling block and its failure message to use a shared or
remaining timeout budget calculated after the preceding waits, or increase the
step timeout to cover the declared maximums and downstream checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c166cb3f-1703-4574-a49c-1cc7989a39d4

📥 Commits

Reviewing files that changed from the base of the PR and between e4958e3 and 18c78e1.

📒 Files selected for processing (1)
  • hack/e2e-chainsaw/_lib/run-kubernetes.sh

Comment thread hack/e2e-chainsaw/_lib/run-kubernetes.sh
@myasnikovdaniil

Copy link
Copy Markdown
Contributor

Looks like fix not working?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E2E is red on this head, and on the two suites this change exists to stabilise, so I have not reviewed the diff yet.

In run 30274169767 both kubernetes-latest and kubernetes-previous failed with fewer than 2 tenant nodes Ready within 18m. What that run captured points away from both halves of the change: the worker DataVolumes reached Phase: Succeeded at Progress: 100.0%, both worker VMIs were Running with Ready true and an address assigned, and the (c) tenant CSRs capture printed No resources found in default namespace. in each suite — no kubelet CSR at all across the full 18 minutes. The guests booted and the widened budget was not the limiting factor; the tenant cilium HelmRelease was still mid-install because no Node had registered.

None of that says the change is wrong. It says this run does not demonstrate the effect the description claims, and it does not support the rationale the diff writes into the code, which attributes the overrun to host storage pressure consuming the margin.

The ask: land a green run on this head, or — if it keeps failing this same way — point the description and the in-code rationale at what the runs actually show, then re-request review.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM — the diff itself is harmless and both halves are defensible, but the rationale it writes into the code names a cause the run on this very head contradicts, and that comment will outlive the PR.

Business context: Stop the VM-heavy chainsaw suites from failing a different case each run by removing the host fsync path that stalls the management etcd, and give tenant worker bring-up a single budget wide enough to absorb it.

Blockers

B1: the 18m rationale attributes the overrun to a margin that the run on this head shows was not the limit

File: hack/e2e-chainsaw/_lib/run-kubernetes.sh:447

Issue: the new comment explains the widening as "Under host storage pressure that margin evaporates … the worker VMIs were created ~2m into this wait, and the guests then had only ~10m to import Talos, boot, register a kubelet and let cilium turn the nodes Ready. They did not make it", concluding "this is load-induced slowness, not a stuck bring-up; 18m restores margin". The run on this head does not support that reading, and it is the run a future reader will check first.

Evidence: run 30274169767 (head 18c78e1, both changes in effect) failed both suites with fewer than 2 tenant nodes Ready within 18m. In that run the worker DataVolumes reached Phase: Succeeded at Progress: 100.0% — the image import, the part host storage pressure would slow, completed. Both worker VMIs were Running with Ready true and an address, so the guests booted with roughly 15 minutes left on the clock. The (c) tenant CSRs capture printed No resources found in default namespace. for each suite: not a late CSR, no CSR at all in 18 minutes. A budget that the guest never gets close to spending is not the quantity that failed. The comment's own reference run points the same way: it describes 30260770694 ending with "zero Nodes registered and the tenant cilium HR still mid-install", which is exactly how 30274169767 ended — with the import finished on time. The same terminal state on both sides of the import delay means the import delay is not what produces it.

Impact: the comment reads as "cause identified, margin restored", so the next person to meet this failure has a documented reason to widen the budget again rather than look at why the kubelet never submits a CSR. That is the same loop the no-retry convention in docs/agents/e2e-testing.md exists to break — a budget increase is not a retry, but it hides a deterministic failure the same way.

Fix: keep the widening, and rewrite its justification around what is measured rather than around the margin: the two terms are CSR issuance and CSR→Ready, and the second one alone has been observed above five minutes, which is what makes 12m structurally tight. Then drop the claim that this removes the flakiness — on this head it does not — and say what it does: it stops the budget from being the first thing to fire, so the next failure lands on the real term.

Non-blocking follow-ups

  1. The widened deadline also applies on the way to a failure, so while the underlying node-join problem is unfixed every red run spends about twelve extra minutes (six per suite) before reporting the same thing. Worth stating in the description as an accepted cost, since the run budget is shared with everything queued behind it.
  2. cache=unsafe never gets its own measurement here. The description's mechanism — guest fsync serialising the management etcd, operators losing their leases together — is specific and checkable, but this head changes two things at once and its only run failed for a third reason, so nothing in CI isolates the cache change. A run that shows the lease-renewal errors gone would make it evidence instead of reasoning.
  3. hack/e2e-prepare-cluster.bats is also touched by #3294 (the per-node data pool grows to 300G there). Whichever lands second will need to reconcile the -drive and qemu-img lines by hand.

The e2e QEMU guests boot with three raw virtio -drive lines that carry
no cache= option, so QEMU defaults to cache=writeback and honors every
guest fsync. The management-cluster Talos etcd fsyncs on each Raft
commit, while containerd unpacks dozens of images and CDI imports
multi-GB VM disks onto the same host-backed raw files during the
VM-heavy suites. The fsync storm serializes etcd's writes into
multi-second stalls, the mgmt apiserver times out lease PUTs, and many
operators (capk, etcd-operator, fdb-operator, cilium-operator, kamaji)
lose leadership at once — failing whichever chainsaw suite is
mid-flight, a different one each run.

Add cache=unsafe to each guest drive so guest flushes become no-ops
against the host page cache, removing the fsync-gated etcd stalls. This
is safe only because these CI VMs are ephemeral and destroyed every
run; never use it for real data.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
The kubernetes-latest and kubernetes-previous suites flaked on the
worker node-join wait under host load. In run 30260770694 both failed at
exactly 12m with zero tenant Nodes registered and the tenant cilium HR
still mid-install: a transient drbd.linbit.com/lost-quorum taint delayed
the worker DataVolume imports, so the KubeVirt VMIs were only created ~2m
into this wait and the guests then had ~10m to import Talos, boot,
register a kubelet and let cilium turn the nodes Ready.

This single deadline absorbs the entire worker bring-up (the
machinedeployment replicas=2 gate clears before the VMIs even exist), so
under storage pressure 12m is too tight. A less-loaded fleet run passed
both suites unchanged, confirming load-induced slowness rather than a
stuck bring-up. Widen the deadline to 18m, which restores margin and
still sits well inside the 40m chainsaw step timeout.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
@kvaps
Andrei Kvapil (kvaps) force-pushed the fix/e2e-qemu-drive-cache-unsafe branch from 18c78e1 to 330acfe Compare July 28, 2026 15:48

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest push (the "fix" commit) is a pure rebase onto the new base, not a content change: both commits kept their original author dates and only the committer dates moved, and the rationale comment at hack/e2e-chainsaw/_lib/run-kubernetes.sh:447-461 is byte-for-byte unchanged. So the blocking concern raised earlier is still open.

Core issue: the comment justifies widening the node-join budget 12m→18m as host-storage-pressure delaying DataVolume imports. But the failing run on this head shows the worker DataVolumes at Succeeded/100%, both VMIs Running+Ready with margin to spare, and the tenant-CSR capture printing "No resources found" — i.e. the kubelet never submitted a CSR across the full 18m. The limiting term was never the import margin; widening the deadline masks a deterministic failure and the misleading rationale outlives the PR.

Please either land a green run on this head, or re-anchor the justification on the measured terms (CSR issuance + CSR→Ready) and drop the "removes the flakiness" claim. The cache=unsafe change itself is fine.

@lexfrei

Copy link
Copy Markdown
Contributor

Raising the deadline is worth doing. There is a second budget next to it that this PR does not touch: the tenant cilium HelmRelease times out its own 10m install, and Flux's default remediation then uninstalls the release, which on its own keeps the nodes from reaching Ready. Details: #3229 (comment)

One caveat on attribution: the suites also pass on the current base without the changes here, so the recent green runs are not evidence either way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the mechanism the widening writes into the code is reproduced on a main-only run, which retires the blocker I raised earlier, and cache=unsafe is a correct fit for guests that get killed at teardown.

Withdrawing my earlier blocker

I blocked on the rationale comment at hack/e2e-chainsaw/_lib/run-kubernetes.sh:447 for naming a cause the runs contradicted. Both pieces of evidence I built that on were wrong.

The sequence the comment describes is reproduced on main without this PR, in the nightly full-suite job 90471679906 from this morning, where both kubernetes suites went red with fewer than 2 tenant nodes Ready within 12m. In kubernetes-latest the two worker disk importer pods logged FailedScheduling ... 0/3 nodes are available: 3 node(s) had untolerated taint {drbd.linbit.com/lost-quorum: }, the worker VMIs were then created at 05:14:19 and 05:14:20 against a wait that started around 05:12 — the comment's "~2m into this wait" — the guest agent connected only at 05:21:27 and 05:22:31, and the deadline fired at 05:24:06 with zero Nodes. Taint delays the import, the guest then runs out of clock: that is the claim, on a run carrying neither of your changes.

My other point — "no kubelet CSR at all across the full 18 minutes" — read the wrong half of capture (c). On a Talos tenant kubectl get csr is empty by design, because node certificates go through the talos-csr-signer sidecar rather than the CSR API. In that same nightly the list prints No resources found while the signer log records both worker certs signed, at 05:21:41 and 05:22:24. An empty CSR list is not evidence of a missing registration attempt, so it cannot carry the argument I hung on it.

Non-blocking follow-ups

  1. 18m buys margin but does not bound the failure. In the same nightly the tenant cilium HelmRelease hit its own 10m install timeout, Flux ran an uninstall remediation, and a second 10m install was still in flight when the deadline fired — InstallFailed ... timeout waiting for: [Deployment/cozy-cilium/cilium-operator status: 'InProgress'] followed by UninstallSucceeded ... uninstall remediation, in both suites and twice for kubernetes-previous. Worst case that cycle is 10m + 10m, so a run can still spend 18m without ever getting a Ready node. Whatever comes after this probably has to touch that install timeout or the remediation policy, not this deadline.
  2. On the inline note about the enclosing 40m step: the budget holds, with numbers. The failing kubernetes-latest suite in that nightly took 1429s (~24m) with the 12m deadline, so 18m puts the worst observed run near 30m, and the green run on this head took 1013s (~17m). No change needed.
  3. cache=unsafe removes the flush gate, not host writeback pressure. The e2e runner has 128 GB and the three guests claim 72 GiB of it, so once dirty pages for the 50G system and 200G data images reach the kernel's dirty ratio, the guest stalls on writeback throttling instead of on fsync. Worth knowing before the next round of "still flaky" gets attributed elsewhere.
  4. The release note says this "removes the cross-operator leader-election flakiness". The phenomenon is real — the same nightly captured management-side cilium-operator lease failures with context deadline exceeded and Failed to renew lease between 03:28 and 03:32, restarts=2 — but nothing in CI isolates the cache change, so that sentence states a hypothesis as a result. Describing what was done rather than what it fixes keeps the changelog honest.
  5. #3294 is open and touches both files this PR changes, not only the bats file. Whichever lands second reconciles the -drive lines and the node-join block by hand.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOT LGTM — correcting the approval I posted a few minutes ago: I approved before checking what 18m actually did on this branch, and it went red six times out of six with the same marker, which contradicts the conclusion the comment writes into the code. Everything else in that review stands, including the withdrawal below.

Still withdrawn: the misattribution charge

My earlier blocker said the comment at hack/e2e-chainsaw/_lib/run-kubernetes.sh:447 named a cause the runs contradicted. That part was wrong and stays withdrawn. The sequence it describes is reproduced on main without this PR, in this morning's nightly full-suite job 90471679906, where both kubernetes suites went red with fewer than 2 tenant nodes Ready within 12m. In kubernetes-latest the two worker disk importer pods logged FailedScheduling ... 0/3 nodes are available: 3 node(s) had untolerated taint {drbd.linbit.com/lost-quorum: }, the worker VMIs were created at 05:14:19 and 05:14:20 against a wait that started around 05:12 — the comment's "~2m into this wait" — the guest agent connected only at 05:21:27 and 05:22:31, and the deadline fired at 05:24:06 with zero Nodes. Taint delays the import, the guest then runs out of clock: that is the claim, on a run carrying neither of your changes.

My other point was worse. "No kubelet CSR at all across the full 18 minutes" read the wrong half of capture (c): on a Talos tenant kubectl get csr is empty by construction, because node certificates go through the talos-csr-signer sidecar rather than the CSR API. In that same nightly the list prints No resources found while the signer log records both worker certs signed, at 05:21:41 and 05:22:24.

Blocker

B1: 18m did not restore margin on this branch — six suites, six times, same marker

File: hack/e2e-chainsaw/_lib/run-kubernetes.sh:447-461

Issue: the comment closes with "this is load-induced slowness, not a stuck bring-up; 18m restores margin". The branch's own CI says otherwise, and the reason it says otherwise is a mechanism the comment does not mention.

Evidence: run 30274169767, head 18c78e12, both halves of this change in effect, three attempts. Every attempt failed both heavy suites, and each carries fewer than 2 tenant nodes Ready within 18m twice — 6 of 6, with suite durations of 1489s/1798s, 1775s/1792s and 1742s/2055s. All three attempts also carry InstallFailed on kubernetes-test-latest-version-cilium and kubernetes-test-previous-version-cilium. That is the loop: the tenant cilium HelmRelease has timeout: 10m and no explicit install.strategy, so Flux's default RemediateOnFailure uninstalls the release between attempts. From the uninstall onward the tenant cluster has no CNI, and a node without CNI cannot reach Ready no matter how much budget is left. This morning's nightly on main shows the same pair of events (InstallFailed ... timeout waiting for: [Deployment/cozy-cilium/cilium-operator status: 'InProgress'], then UninstallSucceeded ... uninstall remediation) in both suites, twice for kubernetes-previous.

Impact: the number is defensible — a 10m install plus a remediation cycle can exceed 18m, so the budget should not be the first thing to fire. The sentence claiming it restores margin is not, and it is the sentence that outlives the PR. The next person to hit this reads "cause identified, margin restored" and widens the budget again instead of looking at the release that keeps taking CNI away.

Fix: keep 18m and rewrite the conclusion around what is measured. The terms are start-of-clock to VMI (2–3m observed), guest boot to first Talos CSR (8m53s–11m46s observed), and CSR to Ready (never observed below ~5m, because no failing run gets there) — the sum is what overruns, not any single term. Then say what the widening does rather than what it fixes: it stops the deadline from being the first thing to fire, so the next failure lands on the cilium install timeout instead. spec.install.strategy.name: RetryOnFailure on that HelmRelease is served by the v2 CRD already in the cluster and would stop remediation from removing CNI; that is the follow-up this comment should point at.

Non-blocking follow-ups

  1. On the inline note about the enclosing 40m step: the budget holds, with numbers behind it. The failing kubernetes-latest suite in this morning's nightly took 1429s (~24m) with the 12m deadline, so 18m puts the worst observed run near 30m, and the green attempt on this head took 1013s (~17m). No change needed.
  2. cache=unsafe removes the flush gate, not host writeback pressure. The e2e runner has 128 GB and the three guests claim 72 GiB of it, so once dirty pages for the 50G system and 200G data images reach the kernel's dirty ratio the guest stalls on writeback throttling instead of on fsync. Worth knowing before the next round of "still flaky" gets attributed elsewhere.
  3. The release note says this "removes the cross-operator leader-election flakiness". The phenomenon is real — the same nightly captured management-side cilium-operator lease failures with context deadline exceeded and Failed to renew lease between 03:28 and 03:32, restarts=2 — but no run isolates the cache change, and the one green attempt on this head sits next to a base that also goes green without these changes. Describing what was done rather than what it fixes keeps the changelog honest.
  4. #3294 is open and touches both files this PR changes, not only the bats file. Whichever lands second reconciles the -drive lines and the node-join block by hand.

@myasnikovdaniil myasnikovdaniil left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a second block — Aleksei Sviridkin (@lexfrei)'s CHANGES_REQUESTED already stands and I am not stacking another on top of it. One substantive ask, plus a scope correction that changes the CI picture enough that it should be on the record before anyone acts on the standing review.

The CI picture moved: the current head is green

The diff content is byte-identical to 18c78e12 (both files hash to the same blobs, author dates unchanged), so the push was a pure rebase — but the base moved 66c96b432d598e672f, and on the current head the heavy suites are 4/4 green. Run 30375214774: attempt 1 (job 90333030340) kubernetes-latest PASS 1069.22s and kubernetes-previous PASS 1261.55s with only chainsaw/harbor failing; attempt 2 (job 90376754751) all 43 suites PASS, 1012.86s and 972.07s. The standing 6 of 6 red at 18m is run 30274169767 on the superseded base. Same diff, opposite outcome — which points at the base, not at either half of this PR.

cache=unsafe: land it as-is

I checked the three things that could make it unsafe and all are absent: nothing on the host ever reads system.img / data.img / seed.img (no consumer anywhere in hack/ or .github/workflows/ outside hack/e2e-prepare-cluster.bats), teardown is a plain kill $(cat srv*/qemu.pid) at line 24 after which nothing reads them, and nothing in the e2e reboots, resets or restarts these guests — so no filesystem is ever re-read after flushes were dropped. Losing unflushed data on a host crash costs a run that was being discarded anyway. Worth noting the delta is narrower than the title reads: QEMU's default here is already cache=writeback, so this only adds cache.no-flush=on, which removes the flush gate but not host writeback throttling — three guests claim 72 GiB (-m 24576 × 3) of a 128 GB runner while the 50G and 200G images are written, so once dirty pages hit the kernel dirty ratio the guest stalls on writeback instead of on fsync. Partial, not ineffective.

The one ask: the rationale comment, not the number

hack/e2e-chainsaw/_lib/run-kubernetes.sh:447-461 attributes the overrun to a drbd.linbit.com/lost-quorum taint delaying the worker DataVolume imports and concludes "this is load-induced slowness, not a stuck bring-up; 18m restores margin". In its cited run 30260770694 (job 89962336210) the taint is real, but in the 12:18:06 event dump the importer pods' FailedScheduling and Scheduled events sit in the same 15m age bucket — under a minute apart — the container Started a minute later, and the imports finished (Succeeded / 100.0% / Import Complete). The ~2m into this wait VMI figure is accurate (12:04:45 and 12:05:08 against a wait starting 12:02:58) but structural, as the comment itself says two sentences earlier when it notes the machinedeployment .status.replicas=2 gate clears while the VMs are still only Machine objects. So the chain taint → delayed imports → margin gone is measurably too small to carry the conclusion.

docs/agents/e2e-testing.md:123 does permit a longer wait when justified in-line, so the widening is not forbidden by convention — but line 112 says to fix the cause rather than widen the timeout, and an in-line justification that names the wrong cause is worse than none, because the sentence outlives the PR and sends the next reader to widen the budget again. Rewriting it around the measured terms — wait-start to VMI ≈2m (structural), VMI to Node-Ready ≲4m on healthy runs, and a failing run that signs no cert at all — and saying what the number does (stops the deadline being the first thing to fire) rather than that it restores margin or removes flakiness, is enough for me.

What the failing runs actually show, for whoever picks up the real cause

Across all seven failing heavy suites I examined (branch attempts 1/2/3, the nightly 90471679906 on main, and the reference run), the worker DataVolumes all reached Succeeded / 100.0% / Import Complete and kubectl describe nodes on the tenant printed zero node output — no Roles:, no Taints:, no node Name: lines, no KubeletNotReady — while kubectl get csr answered and crust-gather completed, so the API was reachable and there were genuinely zero Node objects at every deadline.

The distribution is bimodal. Healthy: node-Ready ≲5m40s into the wait (green attempt 2 kubernetes-latest, bounded via the post-wait coredns-custom 0 8m1s print inside a 900s script — that ConfigMap is resource-policy: keep and its HelmRelease dependsOn cilium, so it cannot predate node-Ready), with node AGE of 60s/57s at the moment the wait exited. Failing: reference run VMIs Running 9m37s/10m at the deadline with zero certs; branch attempt 3 at 15m20s of guest runtime with nothing in the talos-csr-signer log but its two startup lines. ≲4m versus >9m40s-with-no-progress is a wedge, not slowness, and no budget in this range crosses it — which is why I do not think the number is what matters here in either direction.

Where the data does not distinguish two readings: two branch suites did sign certs late (attempt 1 kubernetes-previous at +14m37s/+15m18s, attempt 2 kubernetes-latest at +16m18s), so something was still progressing past 12m there, and the nightly on main signed at +8m35s and +9m35s/+10m18s against a 12m deadline and still had zero Nodes 1m42s–2m25s later. That is equally consistent with "needed a few more minutes" and with "stalled after cert issuance", and one green run cannot settle the tail.

On the cilium remediation follow-up

The cycle is real and I verified it: interval: 5m, timeout: 10m, install.remediation.retries: -1, no strategy (packages/apps/kubernetes/templates/helmreleases/cilium.yaml), and job 90115738417 carries InstallFailed at −14m and −3m14s with UninstallSucceeded … Helm uninstall remediation at −3m8s. But the causal direction looks inverted: with zero Node objects there is no node whose Ready is blocked by a missing CNI, and cilium cannot become Ready with nothing to schedule its operator and agent onto — so the install cycling reads as a consequence of zero nodes, not their cause.

The field itself is available, for the record: helm-controller/api v1.5.1 (this repo's go.mod) defines InstallStrategy and ActionStrategyRetryOnFailure, and cozystack's own PackageReconciler.buildHelmReleaseSpec already sets Install.Strategy.Name = RetryOnFailure on the HelmReleases it creates — so spec.install.strategy is not absent from the surface, though it is worth confirming against the CRD the tenant Flux actually deploys (2.8.x, packages/system/fluxcd/values.yaml:8) before relying on it. Also worth noting #3229, though adjacent, has a different signature — a node that did register and sits Ready=False KubeletNotReady / cni plugin not initialized.

The open question worth its own issue: why does a booted, Running, import-complete guest sometimes never register a Node for 9m40s–15m20s when a healthy one does it in under 4m?

Non-blocking

  1. The release note states an effect as a result ("removing the cross-operator leader-election flakiness"). Nothing isolates it — the lease signal is not comparable across these runs because each failure mode dumps a different diagnostic set (nightly 90471679906: 20 Failed to renew lease; attempt 1 with the change, 90009900608: still 2 plus 2 lost-leader; green runs 0, but they dump almost nothing). Describing what was done rather than what it fixes keeps the changelog honest.
  2. If 18m is kept: every red run now spends ~6 extra minutes per heavy suite, and the worst failing suite already ran ~2055s (34m15s) against the 40m step timeout at hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml:22. The headroom holds but is thinner than "well inside" suggests.
  3. #3294 is open and touches both files here, not only the bats file. Whichever lands second reconciles the -drive lines and the node-join block by hand — I will take that on my side if this one goes first.

# the same suites unchanged, so this is load-induced slowness, not a stuck
# bring-up; 18m restores margin and still sits well inside the 40m step
# timeout (the downstream LB/NFS/ouroboros checks add ~10-15m on the happy
# path).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment names a cause its own cited run measures as under a minute, and its conclusion was contradicted on the identical diff. Flagging rather than blocking — the number I can live with, the stated cause I cannot.

In run 30260770694 (job 89962336210) the drbd.linbit.com/lost-quorum taint is real, but in the 12:18:06 event dump the importer pods' FailedScheduling and Scheduled events land in the same 15m age bucket — under a minute apart — the container Started a minute later, and both DataVolumes reached Succeeded / 100.0% / Import Complete. So the taint did not "delay the worker DataVolume imports" enough to consume anything.

The ~2m into this wait VMI figure is right (VMIs created 12:04:45 and 12:05:08 against a wait starting 12:02:58) but it is structural, not taint-induced — as this comment itself states two sentences earlier when it notes the machinedeployment .status.replicas=2 gate clears while the VMs are still only Machine objects.

And "18m restores margin" was falsified by this branch on this exact diff: run 30274169767, 6 of 6 heavy suites failed with fewer than 2 tenant nodes Ready within 18m, every one of them with imports complete and zero Node objects.

Please rewrite around the measured terms: wait-start to VMI ≈2m (structural), VMI to Node-Ready ≲4m on healthy runs (green attempt 2 bounded via the post-wait coredns-custom 0 8m1s print inside a 900s script), and a failing run that signs no cert at all in 9m40s–15m20s of guest runtime. Then say what the number does — stops the deadline being the first thing to fire — not that it restores margin or removes the flakiness. docs/agents/e2e-testing.md:123 allows a longer wait when justified in-line; line 112 asks for the cause to be fixed rather than the timeout widened, and a justification naming the wrong cause is worse than none.

# bring-up; 18m restores margin and still sits well inside the 40m step
# timeout (the downstream LB/NFS/ouroboros checks add ~10-15m on the happy
# path).
if ! timeout 18m bash -c '

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 18m itself is not demonstrated necessary, in either direction.

On the current base the wait finishes nowhere near 12m: in green attempt 2 (job 90376754751) the kubernetes-latest SCRIPT ran 20:29:07 → 20:44:07 (900s) and the post-wait ouroboros step printed coredns-custom 0 8m1s. That ConfigMap carries helm.sh/resource-policy: keep (packages/system/coredns/templates/coredns-custom.yaml) and its HelmRelease has dependsOn: <release>-cilium (packages/apps/kubernetes/templates/helmreleases/coredns.yaml), so it cannot exist before cilium is Ready, which cannot precede node-Ready. Node-Ready was therefore ≤419s into that script — ≲5m40s into the wait with the ~79s pre-wait measured on attempt 3. Attempt 1 gives ≤449s (coredns-custom 8m34s, script 963s). 12m would have passed both.

On the failing side the budget is not the limiting term either: zero Node objects at every deadline with imports complete, and guests that had already been Running 9m37s–15m20s. Healthy is ≲4m from VMI to Ready; failing is >9m40s with no progress at all. That gap is a wedge, and no value in this range crosses it.

Honest caveat: two branch suites signed certs at +14m37s/+15m18s and +16m18s, so something was progressing past 12m there, and the nightly on main signed at +8m35s and +9m35s/+10m18s against a 12m deadline and still had zero Nodes minutes later — equally consistent with "needed more time" and with "stalled after signing". A single green run cannot settle the tail. My claim is only that no run yet shows 18m turning a red suite green.

If it stays: every red run now burns ~6 extra minutes per heavy suite, and the worst failing suite already ran ~2055s (34m15s) against the 40m step timeout at hack/e2e-chainsaw/kubernetes-latest/chainsaw-test.yaml:22.

-drive file=srv${i}/data.img,if=virtio,format=raw \
-drive file=srv${i}/system.img,if=virtio,format=raw,cache=unsafe \
-drive file=srv${i}/seed.img,if=virtio,format=raw,cache=unsafe \
-drive file=srv${i}/data.img,if=virtio,format=raw,cache=unsafe \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache=unsafe is the right call here and I would land this half as-is. I checked the three things that could make it unsafe and all are absent: nothing on the host ever reads system.img / data.img / seed.img (no consumer anywhere in hack/ or .github/workflows/ outside this file), teardown is a plain kill $(cat srv*/qemu.pid) at line 24 after which nothing reads them, and nothing in the e2e reboots, resets or restarts these guests — so no filesystem is ever re-read after flushes were dropped. Losing unflushed data on a host crash costs a run that was being discarded anyway.

Worth noting the delta is narrower than it reads: QEMU's default here is already cache=writeback, so this only adds cache.no-flush=on. Page-cache behaviour is unchanged, which means it removes the flush gate but not host writeback throttling — three guests claim 72 GiB (-m 24576 × 3) of a 128 GB runner while the 50G and 200G images are written, so once dirty pages hit the kernel dirty ratio the guest stalls on writeback instead of on fsync. Partial, not ineffective. (seed.img is a tiny read-only cidata ISO, so the flag is a no-op there — harmless.)

The one change I would ask for is in the release note, which states an effect as a result: "removing the cross-operator leader-election flakiness". No run isolates that, and the lease signal is not comparable across these runs because each failure mode dumps a different diagnostic set — the nightly on main (90471679906) shows 20 Failed to renew lease lines, attempt 1 with this change (90009900608) still shows 2 plus 2 lost-leader lines, and the green runs show 0 only because they dump almost nothing. Describe what was done rather than what it fixes.

@kvaps
Andrei Kvapil (kvaps) merged commit 0f0204a into main Jul 29, 2026
26 of 27 checks passed
@kvaps
Andrei Kvapil (kvaps) deleted the fix/e2e-qemu-drive-cache-unsafe branch July 29, 2026 17:32
IvanHunters added a commit that referenced this pull request Aug 4, 2026
Bring the worker-pool split branch up to date with main (111 commits),
picking up the node-join and CI fixes it was missing: the 18m tenant
node-join deadline, guest-fsync-off on ephemeral CI disks (#3455),
kubevirt-csi client rate-limit raise (#3428), the Cilium ingress-IP
race guard (#3430), and etcd-operator v0.5.4.

Conflict resolution: main's #3535 (render the talos-reconcile Job for
the default md0 group) modified packages/apps/kubernetes/templates/talos/
talos-reconcile-job.yaml, which this branch deletes because the worker
split moves that Job into the kubernetes-nodes chart. Kept the parent
template deleted and dropped the parent-scoped test
talos_reconcile_nodegroups_test.yaml: the empty-nodeGroups md0-default
gap it guards cannot occur in the per-pool child chart, and migration 54
materialises the implicit md0 into an explicit child HelmRelease.

targetVersion resolves to 55 (migration 54 on top of main's 53).

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation area/testing Issues or PRs related to testing (e2e, bats, unit tests) kind/bug Categorizes issue or PR as related to a bug kind/flake Categorizes issue or PR as related to a flaky test size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants