Skip to content

feat(chart): one-time metadata backfill as a post-upgrade Helm hook#380

Merged
divyasinghds merged 9 commits into
developfrom
feat/metadata-backfill-hook
Jul 23, 2026
Merged

feat(chart): one-time metadata backfill as a post-upgrade Helm hook#380
divyasinghds merged 9 commits into
developfrom
feat/metadata-backfill-hook

Conversation

@divyasinghds

@divyasinghds divyasinghds commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Draft. The one-time, on-update trigger for the pre-cutover metadata backfill. Pairs with data-ingestors #393 (tracebloc-backfill entrypoint) + backend #1166/#1198 (endpoints).

What & why

Datasets ingested before the federated-alignment cutover carry no enriched schema / feature_stats on 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 a tracebloc-backfill entrypoint 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 — a post-upgrade,post-install hook Job. The chart's auto-upgrade CronJob already runs helm upgrade when 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.yaml is the same pattern.)

Wiring

  • Auth: reuses the release's CLIENT_ID/CLIENT_PASSWORD Secret (the same creds jobs-manager uses); the ingestor's APIClient mints 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.)
  • Store: MYSQL_HOST=mysql-client + CLIENT_ENV + tracebloc.proxyEnv, mirroring the ingestion Job so the ingestor's Config resolves the same MySQL + backend endpoint + corporate-proxy egress.
  • Image: resolves the same images.ingestor repo/tag/digest the ingestion path spawns (floating tag + Always, or pinned digest + IfNotPresent).

Safety

  • Best-effort: retries a few times in-pod, then exits 0 regardless, so a backfill problem can never fail the helm upgrade (which would strand the cluster on the old chart). Outcome is in kubectl logs.
  • One-time / idempotent: before-hook-creation replaces 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 with metadataBackfill.enabled=false.
  • Hardened pod: non-root (65534), no SA-token mount (never calls the k8s API), read-only rootfs, all caps dropped, seccomp RuntimeDefault.

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 whose images.ingestor.tag points 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 --strict clean.

🤖 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 --timeout to avoid hook deadline failures.

Overview
Adds a post-upgrade Helm hook Job that runs the ingestor tracebloc-backfill entrypoint after chart upgrades so pre–federated-alignment datasets get enriched global metadata from MySQL and are pushed back to the backend. It is gated by metadataBackfill.enabled (default on), nil-safe for --reuse-values upgrades missing the new key, and not hooked on post-install.

The Job reuses images.ingestor (tag/digest), release CLIENT_ID/CLIENT_PASSWORD, in-cluster MySQL, and proxy env like ingestion jobs. It is best-effort: in-container timeout, always exit 0, backoffLimit: 0, and activeDeadlineSeconds derived from backfillTimeoutSeconds to 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.

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
divyasinghds marked this pull request as ready for review July 23, 2026 12:31
@divyasinghds
divyasinghds requested a review from saadqbal as a code owner July 23, 2026 12:31
Comment thread client/templates/metadata-backfill-hook.yaml Outdated
Comment thread client/templates/metadata-backfill-hook.yaml
…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>
Comment thread client/values.yaml Outdated
Comment thread client/values.yaml
)

- 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>
Comment thread client/templates/metadata-backfill-hook.yaml
`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>
Comment thread client/templates/metadata-backfill-hook.yaml
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>
Comment thread client/templates/metadata-backfill-hook.yaml
…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>
Comment thread client/templates/metadata-backfill-hook.yaml Outdated
…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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread client/templates/metadata-backfill-hook.yaml
…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>
@divyasinghds
divyasinghds merged commit c3cf38e into develop Jul 23, 2026
14 checks passed
@divyasinghds
divyasinghds deleted the feat/metadata-backfill-hook branch July 23, 2026 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants