fix(chart): collect the upgrade path's own logs (backend#2935) - #951
Conversation
When a customer edge stops upgrading we could see THAT it failed and never WHY: helm and refresh were not in classAContainers. The agreement guard could not see them either -- it enumerated three kinds and missed CronJob. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 32de591. Configure here.
saadqbal
left a comment
There was a problem hiding this comment.
Good change, and I appreciate that the body is straight about what this is — it collects, it
doesn't fix, and you say so. Two things before I approve.
The Closes tracebloc/backend#2935 needs to go, and I got this wrong on #948 in a way that has
already cost something. I told that PR cross-repo keywords don't auto-close in this org. They do.
#2935 went CLOSED / COMPLETED at 10:21:13Z, one second after #948 merged — ten minutes before this
PR existed. So the 431 exit-1s, 90% of that ticket, have been sitting with no open ticket at all.
I've filed backend#2961 for them; point this at Part of tracebloc/backend#2935 and #2961 instead,
or it records a second closer on an already-closed ticket.
Second: nothing fails if the fix is removed. I deleted - helm and - refresh from
classAContainers and the guard went green at 4 globs with all 61 telemetry unittests still
passing. Deriving want from the declaration is the right call in itself, but it means the count
moves with the list, so no leg notices a shrink — the guard checks agreement (globs ⊆ deployed
containers), never coverage. One helm-unittest asserting the rendered filelog include list contains
the helm and refresh globs would pin the behaviour instead of the count.
Non-blocking: #922 also bumps Chart.yaml to 1.9.94. Git will not conflict, because you both
wrote the same number and the version gate passed on each against a 1.9.93 base — so nothing
catches it, and whoever lands second ships different chart content under an already-published
1.9.94. The auto-upgrade resolves LATEST vs CURRENT by version string, which is precisely the
surface this ticket is about.
Also non-blocking, more of a follow-up thought: start_at: end is tuned for long-lived files. A
CronJob pod is a new log file every tick with no stored offset, so collection is a race against the
200 ms poll — and the realistic loss isn't the race, it's a collector restart (plausibly caused by
the very helm upgrade being logged), where every pod that completed during the gap is skipped
outright rather than read late. A second filelog receiver at start_at: beginning scoped to these
globs would be safe, since offsets are persisted and the pods get GC'd anyway.
The parts I checked and liked: fail-open by construction, since no pod spec is touched, so a broken
collector can't make an already-failing upgrade path worse. The destination is durable and
off-cluster with a persistent disk queue and unbounded retry, not a pod that gets reaped. Retention
is ~5h for auto-upgrade and ~45min for image-refresh against hourly and 15-minute cadences. And the
redaction floor already covers clientPassword-shaped output, which matters because helm is the
one process holding it. The guard fix is the more durable half — enumerating a subset of the
workload kinds a chart can deploy was the same defect class the file exists to catch, one level up,
and it's now closed for Jobs and CronJobs generally.
Nits: the body says the guard previously understood "only Deployments and StatefulSets" — it handled
DaemonSets too. And the Bugbot summary in the body says "431/hour", which is 24x out; your
values.yaml comment has it right.
…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>
|
@saadqbal — all four taken, and the second one was the important one. Nothing failed if the fix was removed. You're right and my mutation proof didn't cover the regression that matters. I mutated the guard (removing CronJob handling) and watched it redden; you mutated the values list, which is the thing that would actually get lost, and nothing noticed. Agreement (globs ⊆ deployed containers) and coverage (these two are collected) are different properties, and deriving the count from the declaration is exactly what makes a shrink invisible to the first one. That's mutation-proof mistaken for requirement-proof. Coverage is now pinned by name: two The closer. Confirmed: #2935 went CLOSED/COMPLETED at 10:21:13Z, ten minutes before this PR existed. Body now reads Chart version. Bumped to 1.9.95. Worth stating why it isn't caught, since I initially assumed the gate would:
Nits: both fixed. The guard did handle DaemonSets — that was my prose being wrong while Bugbot's quoted summary right below it was correct. And One thing I checked and deliberately left: |
|
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 07d4eaa. Configure here.
saadqbal
left a comment
There was a problem hiding this comment.
Everything I raised is fixed except one, and that one I have to block on rather than note again.
Closes tracebloc/backend#2961 needs to be Part of. I see you repointed the keyword off #2935 after the earlier flag — but that moved it rather than removed it: GitHub's parser returns exactly one target for this PR, #2961, currently OPEN, so merge closes it as COMPLETED. And #2961's acceptance is entirely root-cause work — "root-cause the exit 1 path on the affected edge, using the logs client#951 makes available" — which this PR deliberately doesn't do, as your body says twice. So merging retires the tracking for the 431 by the same mechanism that created #2961 in the first place. Nothing catches it either: the closing-ref gate checks that a ticket is named, not that closing it is correct, and it passed.
I'm blocking on a one-word body edit because #2961 exists to record my treating exactly this as a non-blocking nit on #948. Not repeating that.
The rest is good work, and I re-ran the mutations rather than take the claim: deleting both container names fails 2 of 61 and exits 1, and so does deleting either one alone. Dropping the CronJob branch from _pod_specs reddens with the named globs. The agreement guard still goes green at 4 globs, which is the blind spot you documented as agreement-vs-coverage rather than papering over — the four matchRegex assertions are what's actually load-bearing, and they hold on both the full and partial map.
1.9.95 clears the collision. I swept all five open PRs touching Chart.yaml — #953→1.9.92, #952→1.9.93, #940→1.9.91, #922→1.9.94 — and nothing else wants it.
Your "checked and deliberately not changed" on :127 is right, and I'm glad you wrote out why: release_ns is derived from Deployment/StatefulSet precisely so the node-agents namespace reads as not-release and the cross-tenant leak check keeps firing. Widening it to the full kind set would have been the plausible-looking wrong fix.
Flip the keyword and I'll approve.
|
Fixed — Repointing was the wrong instinct on my side: it moved the keyword instead of removing it, and a keyword pointed at a correct ticket is still a keyword. #2961's acceptance is the root-cause work on the Verified with GitHub's own parser rather than by re-reading the body — That is also the answer to "nothing catches it": Thanks for re-running the mutations instead of taking the claim, and for the |
|
@saadqbal — flipped. Body now reads You're right that repointing moved the problem rather than removed it. #2961's acceptance is root-cause work — "root-cause the And the gate point is the part worth keeping: Blocking on it rather than noting it again was the right call, given #2961 exists because the same thing was treated as a nit on #948. Thanks for re-running the mutations instead of taking the claim, and for the |
# Conflicts: # client/Chart.yaml
|
@saadqbal — flipped, and verified at the level that actually decides it rather than by eyeballing the word. The body now reads
The check that matters is GitHub's own parse, not the prose: Zero targets, so merge closes nothing. Body updated You're right that nothing would have caught it: And noted on why you blocked rather than flagged: |
|
@saadqbal — keyword flipped, and I verified it the way you framed the block rather than by reading the body.
You were right about the mechanism and right that the earlier edit moved the problem instead of removing it. The body now says Also taking the wider point, not just the fix: the Conflict resolved, chart at 1.9.95 — the version you swept. develop had moved again ( Verified on the merged tree under CI's pinned helm v3.15.4: drift 43/43, helm unittest 660/660, full bats 1647/1647. 42 checks pass, 0 fail, 4 still running. Re-requesting review. |
|
/fr-pass Functional review on staging — passed, with direct evidence. Tier-A journey, This repo's change is on the path that run exercised (install / ingest / use-case / train / leaderboard), so this is functional evidence, not an inference from review. |
Summary
The
ap-workspaceauto-upgrade path fails repeatedly and we cannot see why, because the twocontainers that perform it were never in the Class A log-collection set.
values.yaml'sclassAContainersdecides which containers the collector ships. It listed thelong-running workloads and omitted
helmandrefresh— the CronJob and Job that actually runthe upgrade. So the failing thing was the one thing not being recorded.
What the numbers actually say
The ticket originally led with OOM. That was wrong and I corrected it on the issue: across the
observed window the split is 431×
exit code 1against 46×exit 137— OOM is about 9% ofthe failures, not the story. The story is 431 exits whose logs nobody kept.
This PR does not diagnose those 431. It makes them diagnosable.
What changed
helmandrefreshadded toclassAContainers.collector-class-a-agreement.shtaught to read CronJob (spec.jobTemplate.spec.template.spec)and Job pod specs. It previously understood Deployments, DaemonSets and StatefulSets, which is
precisely why it never noticed the two missing containers — the guard could not see the
workload kind the gap lived in.
hardcoded
4it carried. A count that must be hand-edited whenever the list changes is thenext drift waiting to happen (org rule 1).
versionandappVersionequal).Verification
Mutation-proved, with the anchor asserted to apply:
[ERROR] these globs match no container the chart deploys: tracebloc/helm, tracebloc/refreshall 6 Class A globsmake checkgreen;shellcheckclean.What this does not do
It does not fix the auto-upgrade. The next
ap-workspaceupgrade failure after this ships willcarry the CronJob's own logs, and that is the input the root-cause work needs.
Part of tracebloc/backend#2935 (closed 2026-09-01 10:21:13Z by #948)
Part of tracebloc/backend#2961 — NOT
Closes: #2961's acceptance is the root-cause work on theexit 1path, which this PR deliberately does not do (it collects the logs that work will need). Closing it on merge would retire the tracking for the 431 by the same mechanism that created it.Note
Low Risk
Telemetry scope widens to two short-lived upgrade-path jobs; no auth, data-plane, or upgrade logic changes.
Overview
Adds Class A log collection for the auto-upgrade and image-refresh workloads so repeated
exit 1failures on those CronJobs/Jobs are visible in central telemetry, not only as pod exit status.telemetryCollector.classAContainersnow includeshelm(auto-upgrade CronJob) andrefresh(image-refresh Job). Helm unittestmatchRegexchecks pin filelog paths for both in the full Class A test and the partial-map coalescing test—so removing the names cannot pass CI while agreement-only guards stay green.Test/guard fixes:
collector-class-a-agreement.shwalks CronJob and Job pod specs (not only Deployment/DaemonSet/StatefulSet), sohelm/refreshcount as deployed containers. The partial-map glob count is derived fromvalues.yamlinstead of a hardcoded4.Chart
version/appVersion: 1.9.95.Reviewed by Cursor Bugbot for commit 584088e. Bugbot is set up for automated code reviews on this repo. Configure here.
Follow-ups from @saadqbal's review (2026-09-01)
The fix was removable and nothing noticed. Deleting
- helmand- refreshleft theagreement guard green at 4 globs and all 61 telemetry unittests passing. That guard checks
agreement (every glob targets a container the chart deploys), which is a different property from
coverage. Deriving the expected count from the declaration was right in itself, and it is exactly
what makes a shrink invisible to it. Coverage is now pinned by name — two
matchRegexassertions inthe Class A path test, and two more in the partial-map test so a fleet install that sets some
Collector keys but not
classAContainersis covered too. Mutation-proved against his exact removal:dropping either name, or both, 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. Because both are literally
equal git will not conflict, and the version guard only asserts that
version:changed, so itpasses on each — with
strict: falsefleet-wide the second merge never re-evaluates. Both wouldhave shipped different chart content under one published 1.9.94, on the exact surface
(
LATESTvsCURRENTby version string) this ticket is about.Checked and deliberately not changed:
collector-class-a-agreement.sh:127carries a secondkind enumeration (
Deployment,StatefulSet) that looks like the same defect one level down. Itis not. It defines "the release namespace" as where those workloads live, specifically so the
shared node-agents namespace — which holds a DaemonSet — reads as not-release and the cross-tenant
leak check keeps firing (Bugbot on #779). Widening it to the full kind set would silence that guard.
Filed, not built: the
start_at: beginningsecond receiver. A collector restart during the veryhelm upgradebeing logged skips every pod that completed in the gap outright, which is a biggerloss than the 200 ms poll race — but it is a design change to the receiver set, not part of making
these two containers collectable.