feat(chart): one-time metadata backfill as a post-upgrade Helm hook#380
Merged
Conversation
Adds a post-upgrade/post-install hook Job that runs the ingestor image's `tracebloc-backfill` entrypoint (data-ingestors #393, backend #1166/#1198) once per chart upgrade. The auto-upgrade CronJob runs `helm upgrade` when a new chart publishes, which fires the hook — so a client that auto-updates to this release runs the backfill automatically, no operator step. The sweep brings every already-registered pre-cutover dataset up to the federated-alignment metadata shape (enriched schema + feature_stats, recomputed from the persisted MySQL rows) and re-folds any competition built from them, so old competitions/experiments keep working under the new backend alignment code. - Reuses the same credentials Secret (CLIENT_ID/CLIENT_PASSWORD) and in-cluster MySQL (mysql-client) as jobs-manager, plus tracebloc.proxyEnv for corporate egress; resolves the same ingestor image (repo/tag/digest) the ingestion path spawns. - Best-effort: retries a few times in-pod then exits 0 regardless, so a backfill hiccup never fails the auto-upgrade (which would strand the cluster on the old chart). Operators read the outcome via `kubectl logs`. - One-time/idempotent: before-hook-creation replaces the prior run; the sweep skips datasets already carrying new-shape metadata and the upsert is a safe overwrite, so re-runs are cheap. Opt out with metadataBackfill.enabled=false. - Hardened pod: non-root, no SA-token, read-only rootfs, caps dropped. Tests: 7-case helm-unittest (renders by default, hook annotations, image tag/digest modes, cred+MySQL env, hardened pod, disabled-by-flag). Full chart suite green (274). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-upgrade E2E upgrades from the last published release via --reuse-values,
whose stored values predate the metadataBackfill key — so .Values.metadataBackfill
is nil and `.enabled` failed template rendering ("nil pointer evaluating
interface {}.enabled"), the same hazard the images.ingestor keys already guard.
Guard the whole block with `$mb := default (dict) .Values.metadataBackfill` and
read deadlineSeconds/resources through $mb. Absent key => feature off on the
--reuse-values path; the production auto-upgrade uses --reset-then-reuse-values,
which layers in the new default (enabled: true), so the backfill still runs on
the real upgrade path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
divyasinghds
marked this pull request as ready for review
July 23, 2026 12:31
…bot #380) Two Bugbot findings on the metadata-backfill hook: - Medium: the ingestor-tag fallback was "0.5", but values.yaml and jobs-manager's INGESTOR_IMAGE_TAG default to "0.7"; on the fallback path the Job would pull an ingestor predating the tracebloc-backfill entrypoint and no-op. Aligned to "0.7". - High: swallowing the entrypoint exit code didn't make the hook best-effort — activeDeadlineSeconds (1800, and > autoUpgrade.timeout 10m), image pull/OOM, or Helm's wait timeout still fail the Job and thus `helm upgrade`. Now the container bounds ITS OWN runtime with `timeout $backfillTimeoutSeconds` (default 300s, under the 10m Helm timeout) and ALWAYS exits 0, so the pod completes successfully regardless of how the sweep goes. Removed activeDeadlineSeconds (a deadline kill = failed Job = failed upgrade) and set backoffLimit 0. A run that hits the bound stops cleanly and resumes on the next upgrade (the sweep is idempotent / skips already-backfilled datasets). Residual: a container that never starts (image pull) or is OOM-killed can still fail the hook; those are rare, retried on the next hourly auto-upgrade, and mitigated by the generous memory limit + floating-tag Always pull. Test: asserts no activeDeadlineSeconds, backoffLimit 0, and the timeout-wrapped always-exit-0 command. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) - Medium: backfillTimeoutSeconds was 300s = Helm's default --timeout (5m) that a manual `helm install/upgrade` uses. Since Helm counts image-pull + scheduling against its hook wait (outside the in-container `timeout`), a 300s sweep could push a manual upgrade past 5m and mark it failed. Lowered to 180s, leaving ~2m headroom under the 5m default (and far more under the auto-upgrade's 10m). - Low: schematized the new metadataBackfill block in values.schema.json (enabled: bool, backfillTimeoutSeconds: int>=1, resources), matching the autoUpgrade/imageRefresh blocks so helm type-checks it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`timeout "$TIMEOUT" tracebloc-backfill` only sends SIGTERM; if the entrypoint ignores it or is wedged, timeout waits indefinitely, the wrapper never reaches exit 0, and the hook hangs until Helm's --timeout — failing the upgrade, the very mode removing activeDeadlineSeconds was meant to avoid. Add `-k 30` so timeout escalates to SIGKILL 30s after the SIGTERM. Worst-case wrapper runtime is TIMEOUT+30s (210s at the default), still well under the 5m manual --timeout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
180s sweep + 30s SIGKILL grace = 210s worst-case in-container, leaving only ~90s of Helm's default 5m --timeout for a cold ghcr.io pull + scheduling on a manual install/upgrade. Lower backfillTimeoutSeconds to 120s so worst-case in-container is 150s, leaving ~150s for pull/scheduling under the 5m default (and ~450s under the auto-upgrade's 10m). The sweep is idempotent/incremental, so a smaller per-run budget just spreads a large backlog across upgrades. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bot #380) backoffLimit 0 doesn't bound an ImagePullBackOff pod — it stays Pending (never a failed attempt), so with no activeDeadlineSeconds the Job neither completes nor fails and Helm waits its full --timeout before failing the release. The in-container timeout/exit-0 only apply once the container starts, so they don't cover a pod that never runs. Restore activeDeadlineSeconds as a Job-level backstop = backfillTimeoutSeconds + 120s (240s at the default), well under Helm's 5m default, so a stuck/Pending pod (pull failure) fails FAST and the hourly auto-upgrade retries instead of hanging the whole wait. It does not gate a healthy run: the in-container `timeout` exits ~150s in, before the deadline. A genuinely un-pullable image still fails the upgrade (irreducible for a synchronous hook running the ingestor image) but now quickly and self-healing once the registry recovers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ugbot #380) The Job deadline (backfillTimeoutSeconds + 120 = 240) contradicted the values comment's ~150s cold-pull budget: a pull of 90-150s plus a full in-container run (150s) exceeded 240s and DeadlineExceeded a HEALTHY sweep. Since activeDeadlineSeconds is Job wall-clock and includes the pull, it must exceed (cold pull + full run). Re-derive the deadline as backfillTimeoutSeconds + 30s grace + 120s pull = 270s and reconcile the docs: in-container <=150s, cold-pull budget 120s, deadline 270s. A healthy run (warm pull ~instant, or a cold pull up to ~120s + <=150s container) fits; a Pending/stuck pod is still bounded (270s). Sized for the auto-upgrade's 10m --timeout (the target path); manual installs on a cold cluster should pass --timeout 10m. A pull slower than the budget fails that upgrade and self-heals on the next (image cached by then). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4be4357. Configure here.
…bot #380) The hook was post-upgrade,post-install. On a fresh install there are no pre-cutover datasets to backfill, yet Helm waits on hooks even without --wait, so a cold ingestor-image pull / ImagePullBackOff could fail the whole client install (the bash/Windows installers run `helm upgrade --install` under the default 5m timeout and assume a ~10-15s apply). Drop post-install so the hook never runs at install time; a cluster that actually carries pre-cutover data reaches it on its first auto-upgrade (post-upgrade). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
saadqbal
approved these changes
Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What & why
Datasets ingested before the federated-alignment cutover carry no enriched schema /
feature_statson the global-metadata channel, so the backend's combine-time alignment checks and the edge metadata API can't reconcile them — old competitions/experiments that depend on that metadata degrade under the new backend code. data-ingestors #393 adds atracebloc-backfillentrypoint that fixes each old table (GET record → recompute from persisted MySQL rows → POST back, which also re-folds competitions). This PR is the trigger: it runs that entrypoint once, automatically, when a cluster auto-updates to this release.How — a post-upgrade Helm hook
client/templates/metadata-backfill-hook.yaml— apost-upgrade,post-installhook Job. The chart's auto-upgrade CronJob already runshelm upgradewhen a new chart publishes; that fires this hook. So a client that auto-updates to this release runs the backfill as part of coming up — no operator step, no always-on cost.Chosen over a jobs-manager-startup spawn (client-runtime) because the Helm hook is the idiomatic one-time-on-upgrade mechanism, it's declarative (no runtime code coupled to boot), and it fires exactly on the auto-update event the requirement describes. (The existing
ingestor/templates/post-install-job.yamlis the same pattern.)Wiring
CLIENT_ID/CLIENT_PASSWORDSecret (the same creds jobs-manager uses); the ingestor'sAPIClientmints a backend token from them. (The chart holds no static backend token — jobs-manager mints its own at runtime — so the hook uses the client creds. Noted as a dependency on the ingestor's cred-auth path.)MYSQL_HOST=mysql-client+CLIENT_ENV+tracebloc.proxyEnv, mirroring the ingestion Job so the ingestor'sConfigresolves the same MySQL + backend endpoint + corporate-proxy egress.images.ingestorrepo/tag/digest the ingestion path spawns (floating tag +Always, or pinned digest +IfNotPresent).Safety
helm upgrade(which would strand the cluster on the old chart). Outcome is inkubectl logs.before-hook-creationreplaces the prior run each upgrade; the sweep skips datasets already carrying new-shape metadata and the backend upsert is a safe overwrite, so after the fleet backfills once it's a cheap no-op. Opt out per-cluster withmetadataBackfill.enabled=false.Ordering dependency
The Job runs
tracebloc-backfill, which only exists in the ingestor image once data-ingestors #393 is released and the image rebuilt. This chart change should ship in a release whoseimages.ingestor.tagpoints at an ingestor that carries the entrypoint. Until then the hook is best-effort (logs + exits 0) and re-runs cleanly on the next upgrade once the image has it.Tests
client/tests/metadata_backfill_hook_test.yaml(helm-unittest, 7 cases): renders by default; hook + delete-policy annotations; image tag vs digest modes; cred + MySQL env; hardened pod; disabled-by-flag. Full chart suite green (274);helm lint --strictclean.🤖 Generated with Claude Code
Note
Medium Risk
Runs on every upgrade with backend/MySQL access and can mutate global metadata, but failures are swallowed and the sweep is idempotent; manual upgrades on cold clusters need adequate Helm
--timeoutto avoid hook deadline failures.Overview
Adds a post-upgrade Helm hook Job that runs the ingestor
tracebloc-backfillentrypoint after chart upgrades so pre–federated-alignment datasets get enriched global metadata from MySQL and are pushed back to the backend. It is gated bymetadataBackfill.enabled(default on), nil-safe for--reuse-valuesupgrades missing the new key, and not hooked on post-install.The Job reuses
images.ingestor(tag/digest), releaseCLIENT_ID/CLIENT_PASSWORD, in-cluster MySQL, and proxy env like ingestion jobs. It is best-effort: in-containertimeout, alwaysexit 0,backoffLimit: 0, andactiveDeadlineSecondsderived frombackfillTimeoutSecondsto cap stuck pulls without failing the upgrade. Pod hardening matches other one-off jobs (non-root, no SA token, read-only rootfs).values.yaml,values.schema.json, and helm-unittest coverage (metadata_backfill_hook_test.yaml) document and lock in hook annotations, image modes, env, deadlines, and the disable flag.Reviewed by Cursor Bugbot for commit 17ec7cb. Bugbot is set up for automated code reviews on this repo. Configure here.