docs(qos): correct the false Guaranteed-QoS claims and assert the class for real (backend#2872) - #922
docs(qos): correct the false Guaranteed-QoS claims and assert the class for real (backend#2872)#922LukasWodka wants to merge 34 commits into
Conversation
…ss for real (backend#2872) Six claims in this repo said a workload had Guaranteed QoS. None did, and two files contradicted themselves internally -- install-client-helm.sh:38 said "requests == limits (Guaranteed QoS)" while :324 said "Guaranteed QoS is lost by design", and install-k8s.ps1 carried the same pair. The training-envelope claims went stale when backend#2418 made CPU a request-only share weight; nobody noticed because no test tier could assert a QoS class. Corrected: values.yaml, values.schema.json (which SHIPS to users), jobs-manager-deployment.yaml, mysql-deployment.yaml, install-client-helm.sh, install-k8s.ps1. Deleted the false history at values.yaml:964, which called jobs-manager "BestEffort QoS in older releases". It was Burstable from day one: before #66 introduced the resources block the template hardcoded requests cpu 100m / memory 256Mi and limits cpu 500m / memory 512Mi (git show 2d9d013^:client/templates/jobs-manager-deployment.yaml). Requests AND limits set, so never BestEffort. The pod was under-resourced, which is the real cause; the class was never the difference. Amended the ACCURATE comments too. They attributed Burstable to cpu req != lim alone, which is true but incomplete and would send the next fix attempt down a dead end: ComputePodQOS requires requests == limits in every container INCLUDING init containers, and jobs-manager renders an unresourced init-writable-data whenever hostPath.enabled=true while mysql renders an unresourced mysql-format-guard UNCONDITIONALLY. Measured on the rendered chart: equalising cpu buys Guaranteed for jobs-manager only on a CSI cluster, and for mysql nowhere. New scripts/tests/pod-qos-class.{py,bats} DERIVES each pod class with the kubelet rule instead of asserting the values believed to imply it -- the defect shape that let all of this survive. Asserted per hostPath mode, because the mode changes the answer. Six mutations proven to redden; two of them (an ignored memory dimension, a workload silently becoming BestEffort) exposed real gaps in the first version of the suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
…871) The first version of pod-qos-class.py treated ANY resource key as qos-relevant. ComputePodQOS skips everything isSupportedQoSComputeResource rejects, so a container whose only requests are nvidia.com/gpu and ephemeral-storage -- exactly what client-runtime._get_gpu_resources produces for every GPU training pod -- has EMPTY qos-relevant maps and the pod is BestEffort, not Burstable. So the checker would have reported those pods as Burstable and quietly agreed they were fine: the same defect shape it exists to catch. Found while verifying backend#2871 against the real code rather than trusting the ticket. Two tests added (gpu-only -> BestEffort; extended resources alongside equal cpu/memory -> still Guaranteed, so the rule is not "ignore unknown keys"), and the mutation back to the old behaviour reddens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 345ab1d. Configure here.
|
bugbot run |
saadqbal
left a comment
There was a problem hiding this comment.
Much better than the other two legs of #2872 — I checked every corrected claim and they hold, including the cross-repo ones. _training_limits really does strip the cpu limit, test_pod_envelope_sizing.py:132/257 really does assert "cpu" not in limits, and 2d9d013^ really does show requests and limits both set, so deleting the BestEffort history is right. The init-container inventory checks out in both hostPath modes, I reproduced the measured Guaranteed/Burstable pair, and the checker genuinely derives rather than restates — six mutations of my own all reddened it, and the cpu-only one reddening exactly test 7 corroborates your Q2 story.
Four things first.
"Pod-level resources … are the only route that clears both" isn't true, and your own checker says so. Give init-writable-data and mysql-format-guard a requests == limits block and the pod derives Guaranteed. Drop "only", or say "the only route that doesn't require resourcing the init containers". It's in jobs-manager-deployment.yaml, values.yaml and the test-6 comment — one occurrence each.
The suite skips the one pod in this chart that actually claims Guaranteed. values.schema.json:141 and :176 ship "the pod must be Guaranteed QoS (backend#2871): if it is OOM-killed the node stops advertising its GPU resource and every GPU training pod on it goes Pending" — and gpu.devicePlugin appears zero times in pod-qos-class.bats, because the matrix only varies hostPath. Every assertion in the table pins Burstable, so as written you can catch a regression toward Guaranteed and never one away from it — and away from it is the one that takes the node's GPU advertisement down. That's the claim client#919 landed this morning to make true; this is the check that would keep it true, and it's the one pod it can't see. Two more --sets per vendor — it's Guaranteed today, both vendors, both modes.
Test 1 is titled "every workload's QoS class is the expected one" and asserts 6 of the 10 the chart renders — auto-upgrade, egress-reachability-check, image-refresh and storage-assertions-check aren't in it. The BestEffort sweep is population-wide so the dangerous case is covered, but state the subset or assert the census: a new Guaranteed workload leaves the suite green.
pod-qos-class.py is the first standalone .py guard under scripts/tests, and pyyaml-preflight.bats enumerates .sh and .bats only. Stripping your try/except ImportError leaves it 3/3 green, so that guard's own header claim — "the denominator is the tree … a yaml-parsing guard added tomorrow is checked tomorrow" — is now false and unamended. One or name.endswith(".py").
Smaller: CLAUDE.md #3/#6/#9 don't resolve — this repo's CLAUDE.md has no numbered rules. _norm's "stricter than the kubelet, never looser" has one exception: ComputePodQOS skips non-positive quantities, so requests == limits == {cpu: "0", memory: "0"} is BestEffort on a cluster and Guaranteed here — not reachable from this chart, but it's the paragraph asking the reader to trust the bias direction. The bats header says ten claims were false; six were, and the PR body gets that right. Test 1's ! … | grep -q 'BestEffort' passes if the checker crashes, which is the wrong shape for the one assertion carrying the whole population. And double-check "KEP-2837, beta in 1.36" — I had it as 1.34.
Worth saying plainly: the enforcement half is the most valuable thing in this sweep. Neither sibling leg made its claims checkable at all, and a derived class check wired into a required context is the only reason the next false claim reddens instead of shipping. That's why the GPU gap is worth closing before it lands rather than after.
aptracebloc
left a comment
There was a problem hiding this comment.
Not submitting a verdict — @saadqbal's change-request is standing and the PR is currently conflicting with the base. Leaving this as corroboration for the author.
I ran an independent correctness pass and reached the same substantive points Asad did, which is a good signal the enforcement half is sound: pod-qos-class.py genuinely derives each pod's class from the rendered spec (init containers included, pod-level KEP-2837 handled, fails closed on empty render) rather than restating believed values, and it reddens under mutation. The prose correction is directionally right — every workload really is Burstable (unresourced inits like init-writable-data/mysql-format-guard keep it so), and the false Guaranteed claims are now removed.
Gaps worth closing before it lands:
- The one Guaranteed pod is outside the matrix.
values.schema.jsonships "the device-plugin pod must be Guaranteed QoS (backend#2871)", but the suite only varieshostPath.enabled, nevergpu.devicePlugin. Every table entry pins Burstable, so it catches a regression toward Guaranteed and never one away from it — and away is the case that takes the node's GPU advertisement down. Two more--sets per vendor. - The population BestEffort assertion masks a checker crash.
! python3 "$QOS" "$r" | grep -q 'BestEffort'passes when the checker exits non-zero with empty stdout — the wrong shape for the one assertion carrying the whole population. Assert exit status first, then grep. - "the only route that clears both" is false (
values.yaml:1024,jobs-manager-deployment.yaml:198) — resourcing the init containers also reaches Guaranteed, as your ownpod-qos-class.bats:110derives. Drop "only" or qualify. - Test 1 says "every workload" but asserts 6 of 10 (
auto-upgrade,egress-reachability-check,image-refresh,storage-assertions-checkomitted). Assert the census or retitle. pyyaml-preflight.batsenumerates.sh/.batsonly, so this first standalone.pyguard escapes the "denominator is the tree" preflight. Oneor name.endswith(".py").
Smaller: the CLAUDE.md #9/#3/#6 citations don't resolve (this repo's CLAUDE.md has no numbered rules); the headers say "ten places … every one was false" where the body correctly says six; and double-check "KEP-2837 beta in 1.36". Also needs a rebase — it's conflicting with the base. Deferring to @saadqbal's gate.
— drafted with Claude Code
…ackend#2872) Review on rfcs#63 (@saadqbal) caught this sweep swapping one wrong QoS claim for another -- the one thing it cannot afford to do. Three of my own claims here had the same defect: they read as though the derive path's Burstable envelope were the DEFAULT. It is not. DERIVE_JOB_ENVELOPE is off by default (client-runtime jobs_manager.py:2664), and the fallback assigns the literal cpu=1,memory=2Gi to requests AND limits, so those pods are GUARANTEED -- client-runtime says so outright at :2412: "the literal (and a symmetric env-override) has request == limit, so those pods are Guaranteed ... The derive path is deliberately Burstable on CPU instead." There is even a test named test_derivation_disabled_by_default_uses_literal. So values.yaml, values.schema.json (which ships) and the template comment now split by the flag and state BOTH classes, rather than naming one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f1e66d4. Configure here.
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c276d91. Configure here.
aptracebloc
left a comment
There was a problem hiding this comment.
Re-review at 1b581f9. The rebase resolved the conflict (mergeable) and CI is green — thanks. The doc corrections read accurately now, and the new pod-qos-class.py/.bats guard is the right shape: it derives the class with the kubelet's own rule instead of restating a believed one. Two of the five gaps I raised last pass are handled — the BestEffort assertion is now backstopped by the preceding _class_of checks plus the dedicated fails-closed test, and the new .py self-guards its PyYAML import.
Three are still open, and they go to the guard's core promise (it should redden on real drift):
1 — the one Guaranteed pod is outside the matrix (blocking). _render only varies hostPath.enabled; it never sets gpu.devicePlugin. With --set gpu.devicePlugin.enabled=true --set gpu.devicePlugin.vendor=nvidia the chart renders nvidia-device-plugin-daemonset as Guaranteed — the only Guaranteed workload the chart produces, and the one whose class actually bites (lose it → OOM-evicted → the node stops advertising nvidia.com/gpu → every GPU job goes Pending). It's never rendered here, so drift away from Guaranteed reddens nothing, and no real-chart assertion ever exercises a Guaranteed outcome at all — only the synthetic manifests do. Please add a gpu-device-plugin render mode and assert that pod is Guaranteed.
4 — "every workload" pins 6 of 10 (blocking). The hostPath edges test claims every workload, but the chart renders 10 pod-bearing workloads with these values and only 6 are asserted; t-auto-upgrade, t-egress-reachability-check, t-image-refresh, and t-storage-assertions-check are classified and then ignored. A silent Burstable↔Guaranteed change on any of the four stays green — only a flip to BestEffort is caught. (Same as the open Bugbot thread on this file.) Either assert the full rendered set or derive the denominator from the checker's own output so a newly-added workload can't slip through.
3 — the new "only route" claim is itself false (please fix while correcting the others). values.yaml:1026 and jobs-manager-deployment.yaml:198 state pod-level resources "are the only route that clears both." They aren't: resourcing every init container to requests == limits also reaches Guaranteed — your own checker returns Guaranteed for exactly that shape. For a PR whose job is removing false Guaranteed-QoS claims, reword to something like "the only route that doesn't require reserving capacity for short-lived init containers."
Deferring final clearance to @saadqbal + green regardless. Happy to re-review the moment 1 and 4 land.
— drafted with Claude Code
…list (backend#2872) Bugbot, and it is right about the guard I built to close this exact shape. The expected table restated SIX workload names in the bats file. The chart renders TEN: t-auto-upgrade, t-image-refresh, t-egress-reachability-check and t-storage-assertions-check were classified by the checker and then ignored by the assertion, so a silent Burstable<->Guaranteed change on any of them stayed green. And the init-container check tested that two names APPEARED -- a membership test where a set comparison was needed -- so init-mysql-data could vanish with nothing reddening. A restated list inside a guard against restated claims. CLAUDE.md rule 1, in the file that cites it. Now `pod-qos-class.py --expect <file>` compares the render against a declared expectation by SET EQUALITY IN BOTH DIRECTIONS: a workload the chart starts rendering fails until someone classifies it, and a row naming a workload the chart no longer renders fails rather than being satisfied by nothing. The unresourced-init set is asserted exactly, because that set is what decides whether Guaranteed is reachable at all. Three new tests prove the check can fail: a dropped row, a stale row, and a vanished init container each redden. Five mutations proven, including a REAL chart mutation -- stripping resources from image-refresh-cronjob.yaml, one of the four workloads that was previously invisible -- which now reddens both mode assertions. Verified: bats 53/53, helm unittest 641/641, drift 37/37, version guard ✓. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
…(backend#2872) Three things, all from @saadqbal review of client#922. 1. #942 FALSIFIED TWO CLAIMS THIS PR ADDS. Its `wait-for-mysql` is unconditional and unequal on BOTH dimensions (cpu 10m/100m, memory 16Mi/64Mi) with no values key, so `values.yaml` and `jobs-manager-deployment.yaml` were both wrong to say equalising cpu buys Guaranteed for jobs-manager on a CSI cluster. Measured on the merged tree with hostPath off and every resources.* knob equalised: t-jobs-manager Burstable wait-for-mysql:cpu(req=10m,lim=100m); wait-for-mysql:memory(req=16Mi,lim=64Mi) Guaranteed is now unreachable through values on EVERY cluster. Both comments corrected. 2. AND THE GUARD STAYED GREEN THROUGH IT -- this PR subject one turn deeper. The goldens pin the CLASS and the unresourced-init SET; `wait-for-mysql` is resourced, merely unequal, and jobs-manager was already Burstable, so nothing moved. `scripts/tests/qos-reachability.sh` asserts reachability: it equalises every resources.* knob derived from values.schema.json, classifies through pod-qos-class.py (no second copy of ComputePodQOS), and compares per-pod verdicts to a golden by set equality both ways. Proved rather than argued -- adding an unconditional unequal init container to an ALREADY-Burstable pod (the exact #942 shape) leaves all 19 existing QoS tests green and reddens this guard, naming the container. The golden records blocking container NAMES, not quantities, so a resource bump does not churn it. 3. BOTH INSTALLER HEADERS called the training pod BURSTABLE flat. On a GPU edge it is BestEffort: the GPU path requests only nvidia.com/gpu / amd.com/gpu plus ephemeral-storage, and client-runtime `_get_gpu_resources` never reads RESOURCE_REQUESTS / RESOURCE_LIMITS there. backend#2871 raised both GPU BestEffort workloads; client#919 fixed the device-plugin half and the issue was CLOSED with the training half unfixed, so the record is written into the two files rather than left as a reference. Chart 1.9.94. drift 43/43, helm unittest 657/657, QoS 19/19, Pester 887/887. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # Makefile # client/Chart.yaml # scripts/manifest.sha256 # scripts/tests/pyyaml-preflight.bats
|
All three addressed in b840c69 + the develop merge. Measurements, not prose: 1. #942 falsified two claims — corrected, and you were right about the mechanism. It is the sole remaining blocker, so Guaranteed is unreachable through values on every cluster now, not just off-CSI. Both 2. Reachability is assertable, and the guard proves the blindness rather than asserting it. New The evidence you asked for — I added an unconditional, resourced-but-unequal init container to an already-Burstable pod, which is precisely the #942 shape: Two design notes. The golden records blocking container names, not quantities — my first version pinned the classifier's full reason string, so bumping squid's cpu limit would have reddened a QoS guard and trained the next person to refresh the golden without reading it. And its scope is stated in the file: 3. Both installer headers now carve out the GPU case. Worth flagging what I found chasing your "no thread or ticket tracks it": backend#2871 raised both GPU BestEffort workloads, client#919 fixed the device-plugin half, and the issue was closed with the training-pod half unfixed — so there was no open record at all. Rather than reopen a closed ticket I wrote the mechanism into both files, since that is what survives a merge. Chart is 1.9.94, one above develop's 1.9.93. You were right that the gate had gone stale — I re-ran it rather than reading the green tick and it was red, exactly as you said. On your #948-first ordering: agreed and no objection. This branch has already absorbed #911 and #942 from develop; if #948 lands first I will reconcile the Chart.yaml collision the same way. drift 43/43 · helm unittest 657/657 · QoS 19/19 · preflight 3/3 · check-facts 14/14 · Pester 887/887 · reachability 20/20. @aptracebloc — your CHANGES_REQUESTED from 12:22 on 08-31 is still the other standing block; the three later notes went in as COMMENTED, which does not dismiss it. |
|
bugbot run |
…end#2872) autoUpgrade, imageRefresh and egressProxy each expose requests and limits under their own parent, so they were never equalised and their pods read blocked while an operator could already reach Guaranteed through values. The guard was agreeing with its own incomplete domain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
…blocked (backend#2872) Builds on 691841e rather than replacing it -- the recursive walk and the fatal/informational note split are that commit, and they are right. One verdict was still wrong: `telemetryCollector.resources` is `{"type": "object"}` with no declared keys, so it fell into #NOTAPAIR and t-telemetry-collector was recorded `blocked`. But the schema does not forbid keys either, and the DaemonSet renders the node with `toYaml $tc.resources` -- a wholesale passthrough -- so a requests/limits pair set there reaches the pod. Measured, with ONLY that key equalised: t-telemetry-collector Guaranteed every container has requests == limits otel-collector {requests: {cpu: 1000m, memory: 1Gi}, limits: {same}} So `blocked` was a verdict reached by never having looked -- the one thing this guard exists to refuse. Free-form nodes are now PROBED with the canonical pair and the run reports that the verdict came from a probe rather than a declaration. `gpu.devicePlugin.*` stays #NOTAPAIR: its flat one-pair shape is applied to both sides by construction (client#919 makes the split form unexpressible), so there is genuinely nothing to equalise. Golden: two rows corrected, with the reason recorded beside them. Mutation-proved: disabling the probe branch returns both rows to `blocked` and reddens the guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 70f7091. Configure here.
saadqbal
left a comment
There was a problem hiding this comment.
All three of my holds are cleared, and the reachability one is actually pinned rather than
reworded. qos-reachability.sh equalises every resources.* knob the schema exposes — derived by
walking for the name at any depth, so a knob added later is covered — and set-compares the verdicts
both ways, so t-jobs-manager blocked wait-for-mysql is an assertion now instead of a sentence.
It's in DRIFT_GUARDS, and I checked the drift job log rather than the green tick: 20 verdicts
matched, and it really ran. Pinning blocker names rather than quantities is the right call — a
squid cpu bump won't redden it, but a new blocker appearing will, which is precisely what #942 did.
The expectation file opening with "WHY THIS FILE EXISTS — #942 added wait-for-mysql, so
'equalising cpu buys Guaranteed on CSI' became false" is the part I'd point people at. You didn't
just fix the claim; you recorded the defect that produced it, in the artifact that now prevents it.
Installer headers both carry the GPU carve-out and record that backend#2871 closed with the
training-pod half unfixed — that missing record is now in the tree. Manifest regenerates clean off
the merged tree, 1.9.94 is ahead of develop's 1.9.93, no conflicts. Locally: 19/19 QoS, 20/20
reachability, 657/657 unittest across 37 suites, helm lint clean.
One loose end, small. The Medium on pod-qos-class.py:90-97 is resolved but not fixed — the reply
on that thread describes the qos-reachability.sh walk, which is a different finding. The pod-level
KEP-2837 branch short-circuits before the cpu/memory-only carve-out, so an envelope of nvidia.com/gpu
ephemeral-storageand nothing else returns Burstable where the kubelet says BestEffort. The
container path has theany_setguard; the pod-level path doesn't. Nothing renders pod-level
resources today so it's latent — but it is exactly the "guard hides a BestEffort demotion" shape
this PR exists to close, one turn deeper. Three lines or a ticket, your call; not a blocker from me.
Not approving only because @aptracebloc's change-request from 08-31 12:22 is still standing.
latestOpinionatedReviews returns it; the three later COMMENTED reviews hide it in latestReviews
but don't dismiss it. My side is clear — that one needs an actual approve from them.
Heads up on #948: it took the cpu advice, so both CronJobs go Guaranteed and 10 rows across all
five class goldens move (t-auto-upgrade and t-image-refresh, Burstable -> Guaranteed), taking
the bats suite 19/19 -> 15/19. The reachability golden doesn't move — both already read reachable.
Land this one first and let #948 carry the golden update in its own PR: it's the change that moves
the class, so it's the right owner, and it's what your own guard's failure message asks for. #948
also needs a chart re-bump; it's on 1.9.93, which develop already is.
…ckend#2872) KEP-2837 pod-level resources short-circuit above the container walk, so an envelope of extended resources only returned Burstable where the kubelet says BestEffort. The container path already carried this guard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@saadqbal — took the loose end rather than ticketing it; it was three lines. You're right that the thread was resolved but not fixed: the reply on it described the if not any(d in req or d in lim for d in DIMS):
return "BestEffort", "pod-level resources set, but none of them are cpu or memory"Two cases, not one, because the fix must not become "ignore pod-level resources entirely":
The first is built so it cannot pass for the wrong reason: its containers are perfectly Guaranteed, so if the pod-level branch ever stopped short-circuiting, the case would read Guaranteed rather than quietly still passing. BestEffort can only come from the carve-out. Mutation-proved, and it isolates: deleting those two lines reddens case 17 only — case 16, the container-path equivalent, stays green. That is what shows the two halves are independently guarded rather than one test covering both by accident.
Agreed on the rest — not asking you to approve. @aptracebloc's change-request from 08-31 12:22 is still standing and |
saadqbal
left a comment
There was a problem hiding this comment.
CI is red on your own QoS suite — Unit tests and bats — and it isn't your code. #948 merged at
10:21, four minutes before your push. Your goldens are derived against develop, so
t-auto-upgrade and t-image-refresh now come out Guaranteed (948 pinned cpu as well as
memory on both CronJobs) where all five pod-qos-expect.* files still say Burstable. Ten rows,
two per file. Reproduced both ways: 21/21 on your head, 17/21 on the merge ref.
I'd told you #948 would carry that update. It landed first, so that call is superseded by events
and it's yours now — merge develop in and flip the ten rows. Nothing else shifts: reachability
still 20/20, helm unittest 660/660, lint clean, no rows added or removed, class values only.
I also had #951 wrong: I expected it to move your goldens too, and it doesn't. It touches no
template at all — only Chart.yaml, a telemetryCollector.classAContainers value, and its own
checker. So the only thing it collides with is the version: you're both on 1.9.94 against develop's
1.9.93, so whoever lands second re-bumps.
The pod-level KEP-2837 carve-out is properly fixed, and I checked it by feeding the envelope
through rather than reading the diff: pod-level nvidia.com/gpu + ephemeral-storage returns
BestEffort now with the reason "pod-level resources set, but none of them are cpu or memory",
where the parent returned Burstable. Both halves of the rule use the same guard expression now,
which is the part that makes it stay fixed.
One process note worth saying out loud, because it's the shape this PR exists to close. That thread
was resolved nine seconds after Bugbot filed it, with a reply describing the recursive-walk fix
instead — and the real fix landed 27 minutes later. Right outcome, reached late, but the thread
claimed done before the code was. Different failure from Bugbot clearing its own threads
(backend#2945); this one was a human resolving on a misread.
Not re-firing a change-request — mine already stands, and @aptracebloc's from 08-31 12:22 is still
standing too, behind four later COMMENTED reviews. So reviewDecision won't move on my say-so
regardless; that one needs them.
Worth noting the coupling firing today is evidence the suite works. #948 changed two CronJobs'
resources and this tier noticed within minutes — that's exactly the signal that didn't exist before
this PR, and it caught it on a PR that wasn't yours.
… too (backend#2872)" This reverts commit 51681eb.
|
Reverted my pod-level carve-out — What happened, and it was my error rather than a flake. The three-line fix plus two bats cases passed 21/21 locally, then failed four Root cause of the bad verification: CI pins What I could establish, using the real backend#2962 carries the defect, @saadqbal's original diagnosis, and the three things the next attempt has to do first — starting with reproducing the four failures under helm 3.15.4 on a clean tree, because if they fail there too then the break was never this fix. This PR is back to |
…nd#2872) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ust agreed with (backend#2961) @saadqbal removed `- helm` and `- refresh` from `classAContainers` and nothing went red: the agreement guard passed at 4 globs and all 61 telemetry unittests still passed. The fix was removable without one leg noticing. The cause is that the guard checks AGREEMENT -- every glob targets a container the chart really deploys -- which is a different property from COVERAGE. Deriving the expected count from the declaration was right in itself, and it also means the count moves WITH the list, so a shrink is invisible to it by construction. Coverage has to be pinned by name. Two matchRegex assertions in the existing Class A path test, and two more in the partial-map test so a fleet install that sets some Collector keys but not `classAContainers` is covered too. Mutation-proved against the exact removal that motivated this: dropping either name, or both, now fails 2 tests; restored, 657 pass. Chart 1.9.94 -> 1.9.95: #922 also wrote 1.9.94 against a 1.9.93 base, and because both are literally equal git will not conflict and the version guard -- which only asserts `version:` CHANGED -- passes on each. With `strict: false` fleet-wide the second merge never re-evaluates, so both would ship different chart content under one published 1.9.94. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#2872) The four QoS failures were real and were NOT visible on the branch head. CI evaluates the pull_request MERGE of head into base, and #948 (526f4ad) landed on develop after this branch last merged it: pinning CronJob memory to requests == limits moves t-auto-upgrade and t-image-refresh from Burstable to Guaranteed. On the head alone every case passes, which is why it first read as an environment difference; reproduced by merging develop locally. Measured under CI pinned helm v3.15.4 on the merge -- both CronJobs Guaranteed on every profile -- and all five expectation files updated. Nothing in the chart prose called the CronJobs Burstable, so no comment needed correcting. Rows re-derived from measurement rather than restored from a pre-revert copy: doing the latter would have reintroduced the TEST that 2d25d1a reverted without its code. That revert and b0e4500 diagnostic both stand untouched. Separately worth recording: local helm here is v4.1.1 while CI pins v3.15.4, so chart renders verified locally were not on the CI configuration. Everything above ran under 3.15.4. drift 43/43, helm unittest 660/660, full bats 1647/1647 -- on the merge, v3.15.4. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saadqbal
left a comment
There was a problem hiding this comment.
Goldens were the cause — CI is at 0 failures now with the carve-out still out, which is the proof it never reddened anything. So backend#2962 doesn't need the helm-3.15.4 leg: the fix was innocent and can go straight back.
One thing I don't think the revert meant to do: 2d25d1a took the two bats cases with it, not just the 15 lines in pod-qos-class.py. So the pod-level BestEffort demotion is now latent and uncovered — nothing reddens if it regresses, and a ticket is the only thing holding the memory. I'd restore the cases even if you want to land the .py half separately, since they're what makes the claim checkable.
Unrelated, don't read that 11:07 Medium as addressed — cursor[bot] resolved its own thread with no reply (backend#2945, this is the 7th). The finding itself looks real: the coverage loop does keep going after a non-zero helm template.
Leaving my change-request up for the missing cases, not for the goldens — those are right.
… did NOT reproduce (backend#2872)
Bugbot Medium: the coverage loop called `_render` in five case arms with no
`|| return 1` -- the only `_render` calls in the file without it -- so a failed
render fell through and whatever it left behind was measured for coverage.
THE MECHANISM AS DESCRIBED DOES NOT REPRODUCE, and that belongs on the record
rather than being implied away. The finding said `helm template` streams, so a
failed render can still emit earlier pod documents. Measured on the pinned
v3.15.4 AND on v4.1.1, across three failure modes -- a late template `fail`, a
values-schema violation, and invalid YAML in the rendered output -- helm buffers
the whole manifest and writes 0 bytes every time. With an empty file the
classifier already refuses ("no pod-bearing template in this render") and the
`--sources` call already carried `|| return 1`.
Fixed anyway, because "safe" was resting on two accidents this test asserts
neither of: helm buffering, and the classifier refusal. A helm that ever did
stream would reopen it in silence, and one word per arm is cheaper than that
dependency.
Mutation-proved: a late `fail` in requests-proxy-service.yaml now reddens
"coverage: every pod-bearing template is classified by at least one mode".
bats-hygiene 14/14 accepts the form.
drift 43/43, helm unittest 660/660, full bats 1647/1647 under v3.15.4 on the merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
bugbot run |
|
Bugbot Medium "Coverage ignores failed helm renders" — fixed, but the mechanism as described does not reproduce, and that belongs on the record rather than being implied away. The gap was real: those five The stated cause was that helm buffers the whole manifest and writes 0 bytes on failure. And with an empty file the classifier already refuses — Fixed anyway, in five words, because "safe" was resting on two accidents the test asserts neither of: helm buffering, and the classifier refusal. A helm that ever did stream would reopen it silently. Mutation-proved: a late On the four QoS failures you flagged — root cause found, and it was not an environment quirk. CI evaluates the Also worth knowing for anyone verifying chart work locally: local helm here is v4.1.1 while CI pins v3.15.4. Renders "verified locally" were not on the CI configuration. Everything in this comment ran under 3.15.4 against the merge commit. drift 43/43 · helm unittest 660/660 · full bats 1647/1647. |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 033144e. Configure here.
Summary
Part of tracebloc/backend#2872 — the
clienthalf. Sibling PRs follow forcli,client-runtime, andrfcs.Six places in this repo claimed a workload had Guaranteed QoS. None did. Two files contradicted themselves internally:
scripts/lib/install-client-helm.sh:38"requests == limits (Guaranteed QoS)":324"Guaranteed QoS is lost by design"scripts/install-k8s.ps1:4510same claim:333same correctionThe training-envelope claims went stale when backend#2418 made CPU a request-only share weight. Nobody noticed because no test tier could assert a QoS class:
status.qosClassis computed by the API server, sohelm unittestcannot express it, andgrep qosClass client/tests/returned zero hits. What existed instead were assertions on the values believed to imply the class — which is exactly how a wrong claim about a class stays green for months.Corrected
values.yaml,values.schema.json(which ships to users — done first for that reason),jobs-manager-deployment.yaml,mysql-deployment.yaml,install-client-helm.sh,install-k8s.ps1.Deleted the false history at
values.yaml:964, which described jobs-manager as "under-resourced, BestEffort QoS in older releases". Verified against git rather than taken on trust — before #66 introduced the resources block the template hardcoded:Requests and limits set, unequal → Burstable, from day one. BestEffort requires neither, and jobs-manager has never had neither. The pod was under-resourced, which is the real cause; the QoS class was never the difference.
The accurate comments were amended too
They attributed Burstable to
cpu req != limalone. True but incomplete, and the omission would send the next fix attempt down a dead end — equalise cpu, watch nothing change, conclude the chart is broken.ComputePodQOSrequires requests == limits in every container, init containers included. Measured on the rendered chart:true(installer edges)jobs-manager/init-writable-data,mysql/init-mysql-data,mysql/mysql-format-guardfalse(CSI)mysql/mysql-format-guardSo equalising cpu buys Guaranteed for jobs-manager only on a CSI cluster, and for mysql nowhere —
mysql-format-guardis unconditional. That last point makes theresources.mysql.limits.cpuoverride actively harmful rather than merely unnecessary: an operator setting it gets the CPU throttling the chart itself warns causes InnoDB lock-wait timeouts, and no QoS change at all. The template now says so.This is one finding beyond the ticket: it named
mysql-format-guardbut notinit-mysql-data, so mysql has two unresourced inits on installer edges, not one.Test plan
New
scripts/tests/pod-qos-class.{py,bats}. The design point: it derives each pod's class with the kubelet's own rule from the rendered spec, rather than asserting values believed to imply a class — the defect shape that let all ten claims survive. Picked up automatically bybats scripts/tests/*.batsin Standard checks; renders the real chart likechart-pull-secret.batsalready does; no cluster needed.Asserted per hostPath mode, because the mode changes the answer — a single-mode check would have agreed with the wrong comments on exactly the clusters the installer provisions. Pod-level resources (KEP-2837, beta 1.36) are handled, so the checker follows the class when the chart adopts them rather than needing a rewrite.
Fails closed: an empty render is a finding, not agreement.
Six mutations, each proven to redden:
Two of those exposed real gaps in the first version of the suite, which is the reason for running them: Q2 stayed green because no chart pod has cpu equal and memory unequal, so the expected table could not see a dropped memory dimension — fixed by deriving the input domain (both dimensions, both directions) rather than relying on the table. Q5 stayed green under my first attempt because I mutated one of jobs-manager's two containers, leaving the pod Burstable; re-run against a single-container workload it reddens. An inert mutation and good coverage look identical in a log, so the anchor is asserted on every one.
Verified:
helm unittest641/641,helm lintclean,bats73/73 across the four affected suites,shellcheck -S warning -xclean,bash -nclean,check-facts.shclean, chart version guard ✓, installer manifest regenerated (scripts/gen-manifest.sh).Checklist
developclient/Chart.yamlversion + appVersion bumped to 1.9.87scripts/manifest.sha256regenerated for the two installer-script edits🤖 Generated with Claude Code
Note
Low Risk
Comment, schema description, and test-only changes with no intended runtime chart behavior change; risk is mainly operator confusion if docs were wrong—which this PR addresses—and CI failing until goldens are updated when QoS-related templates change.
Overview
Fixes incorrect Guaranteed QoS claims across the chart, schema, templates, and installers, and adds automated checks so those mistakes cannot stay green silently.
Documentation now states that control-plane pods stay Burstable (unequal CPU, unresourced or unequal init containers such as
wait-for-mysql,init-writable-data,mysql-format-guard), that mysql cannot be Guaranteed viaresources.mysql.limits.cpu, and how spawned training jobs classify: CPU with derivation off → Guaranteed; derivation on → Burstable; GPU → BestEffort. Installer headers andDERIVE_JOB_ENVELOPEprose are aligned with backend#2418/#2871/#2872.New guardrails:
pod-qos-class.py+pod-qos-class.batsderive QoS from rendered Helm output (per hostPath, GPU vendor, egress lockdown) against golden tables;qos-reachability.shis wired intomake driftto record whether Guaranteed is reachable through values after equalising all schemaresourcesknobs.pyyaml-preflight.batstreats standalone.pyguards correctly.Chart version 1.9.94; installer manifest.sha256 updated. No intentional change to rendered workload specs beyond comments/schema text.
Reviewed by Cursor Bugbot for commit 033144e. Bugbot is set up for automated code reviews on this repo. Configure here.