fix: tlc drive core capping and address linting issues - #2792
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Graphite Automations"Add anton/matt/sergey/kristina as reviwers on operator PRs" took an action on this PR • (09/03/26)2 reviewers were added to this PR based on Anton Bykov's automation. |
|
Claude finished @kristina-solovyova's task in 3m 53s —— View job Review: tlc drive core capping + lint fixes
VerdictThe core fix is correct, and the reasoning in the new comments checks out against the code. No blocking issues. Two inline notes posted, both non-blocking. The TLC cap (
|
| case | tlc side | qlc side | sum |
|---|---|---|---|
tlcDerived <= assigned |
tlcDerived |
assigned - tlcDerived |
assigned ✓ |
tlcDerived > assigned |
assigned (capped) |
max(assigned-tlcDerived,0) = 0 |
assigned ✓ |
qlcGiB <= 0, assigned > 0 |
assigned |
0 |
assigned ✓ |
tlcGiB <= 0, assigned > 0 |
0 |
assigned |
assigned ✓ |
assigned == 0 |
TlcDriveCores |
QlcDriveCores |
capacity-derived (intended fallback) ✓ |
tlc + qlc == assignedCores now holds in every assigned branch. Without the cap the mixed case could exceed it, and since totalTlcDriveCores feeds both the total hard floor and the ratioed term of RequiredComputeCores (cores.go:60-72), the over-count propagated straight into over-stated compute. Real bug, real fix.
I also confirmed the cap can only bind for existing un-grown containers: both the grow path (planner.go:600) and the new-container path (planner.go:617) run pinCores, whose derived = RequiredDriveCores(tlc, qlc, cons) is ceil(tlc/tlcPer) + ceil(qlc/qlcPer) — always ≥ the TLC share alone. So the premise in the new comment holds, modulo the wording nit in my inline note.
Test coverage
cores_test.go is well-targeted and the arithmetic in every case is right (testCons() is TlcCapacityPerCoreGiB = 5*tib = 5120, QlcCapacityPerCoreGiB = 50*tib = 51200, so 10240 → 2 and 51200 → 1 as the comments claim). The tlc+gotQlc == assigned assertion is the right invariant to pin.
Suggestion (optional): the test's own header comment explains that an over-count "reaches totalTlcDriveCores and from there both terms of RequiredComputeCores" — but nothing tests at that level. A single case feeding a mixed ExistingContainer with stale NumCores through totalTlcDriveCores → RequiredComputeCores would lock in the behaviour the fix actually exists to protect, and would catch a future regression in finalCores/finalPoolCap that the leaf-level tests can't see. Fix this →
Validation changes
cluster_min_drives_feasibility.go— dead-branch removal is correct;derivedSizingMode(cluster_sizing_mode_flip.go:94-99) documents the same nil-receiver contract. See inline note on the resulting inconsistency withcluster_signed_drives.go:32-36.cluster_drive_compute_core_ratio.go— the new "advisory is inert for drive-sharing" comment is verified accurate. WithComputeToTlcDriveCoreRatio = 1.0(internal/config/env.go:537),required == driveSide, and line 45 has already returned forcomputeSide < driveSide, so the advisory can never fire in drive-sharing mode. Documenting this rather than silently leaving a validator that never triggers is the right call, and the paired test-comment update explains why the test pins 2.0.
Lint fixes
All mechanically sound, no behaviour change:
plan.go,collect.go,planner.go,mergeRoleNodes— index-based ranging over[]NodeCapacityto dodgerangeValCopy. Checked for aliasing bugs:mergeRoleNodesbuildsinventoryfromappend([]NodeCapacity(nil), driveInv...)(fresh backing array) and appends*ncby value, so the pointer intocomputeInvnever escapes. Same forplanner.go:521—nc: *nccopies into thenodeStatevalue field.drainingComputeClause—in.remainingismap[string]NodeCapacity(value type), so the rewrite genuinely avoids the struct copy. It does cost a second map hash per iteration; irrelevant at this scale.pkg/util/hashes.go—reflect.Ptr→reflect.Pointer. Confirmed noreflect.Ptroccurrences remain anywhere underpkg/,internal/,cmd/.
Not verified
I could not run go build ./... or go test ./internal/capacityplanner/... ./internal/validation/... — both were denied by the tool permission policy in this environment. Everything above is static analysis of the diff and the surrounding code. Adding Bash(go build:*) and Bash(go test:*) to --allowedTools would let me confirm the new tests actually pass.
• 09-03-fix_tlc_drive_core_capping_and_address_linting_issues
| tlc := TlcDriveCores(tlcGiB, cons) | ||
| // Only an existing container can need the cap: pinCores keeps new ones at or above the derived value, | ||
| // so a shortfall means the assigned count predates the current TlcCapacityPerCoreGiB. | ||
| if assignedCores > 0 { | ||
| return min(tlc, assignedCores) | ||
| } | ||
| return tlc |
There was a problem hiding this comment.
The cap itself is right — it restores tlc + qlc == assignedCores for mixed containers, which is what totalTlcDriveCores/totalQlcDriveCores feed into both terms of RequiredComputeCores. Two notes:
1. The comment is slightly narrower than reality. Grown containers also go through pinCores (planner.go:600), so the precise statement is "only an existing un-grown container can need the cap" — a grown one is re-pinned at or above RequiredDriveCores(newTlc, newQlc), which already includes the TLC share. Suggest tightening the wording so a reader doesn't go looking for a growth-path hole that isn't there.
2. When the cap binds, QLC gets attributed zero cores. qlcDriveCoresForContainer computes max(assignedCores-tlc, 0) against the uncapped tlc, so for assigned < tlcDerived the split is (assigned, 0) — the QLC pool contributes nothing to compute sizing despite holding capacity. That's harmless at shipped defaults (ComputeToQlcDriveCoreRatio = 0.0, internal/config/env.go:538), and it's the only split that preserves the sum invariant, so I think it's the right call. But if an operator ever sets CAPACITY_COMPUTE_TO_QLC_DRIVE_CORE_RATIO above the TLC ratio, this under-states required compute rather than over-stating it. Worth one line in the comment recording that the whole shortfall is charged to TLC deliberately.
| // A nil dynamicTemplate IS auto-full-drives mode (UsesAutoFullDrives returns true on a nil receiver), | ||
| // so it takes the per-node branch rather than falling through as "nothing configured" — which is also | ||
| // what lets everything below dereference Dynamic unguarded. | ||
| if cluster.Spec.Dynamic.UsesAutoFullDrives() { | ||
| return validateMinDrivesAutoFullDrives(ctx, c, cluster, minNumDrives, fldPath) | ||
| } |
There was a problem hiding this comment.
Correct — derivedSizingMode (cluster_sizing_mode_flip.go:94-99) documents the same nil-receiver invariant, so line 46's cluster.Spec.Dynamic.DriveContainers is only reached with a non-nil Dynamic, and the removed branch was genuinely dead.
Two things worth being aware of rather than changing:
- The safety now rests entirely on
UsesAutoFullDrives()'s nil-receiver contract, which lives in the externalweka-k8s-apimodule.cluster_signed_drives.go:32-36keeps an explicitDynamic == nilguard with a comment saying the branch is redundant but "keeps the field reads below obviously safe" — so the codebase is now inconsistent on this. Either is defensible; picking one and applying it to both would be nicer than having each file argue the opposite case in a comment. validateMinDrivesAutoFullDrivesstill needs its ownDynamic != nilguard at line 86, and correctly keeps it. Good.
34c2de7 to
c32fd4b
Compare
|
Claude finished @kristina-solovyova's task in 52s —— View job Review: no changes since last review — skipping
This All 11 post-image blob SHAs match the previously reviewed set exactly:
Diffstat is likewise unchanged at My earlier review and its two inline comments (on Not run: • |


No description provided.