Skip to content

Security / performance / scalability hardening (PRD-68) - #93

Merged
jicowan merged 10 commits into
mainfrom
feature/review-hardening
Sep 3, 2026
Merged

Security / performance / scalability hardening (PRD-68)#93
jicowan merged 10 commits into
mainfrom
feature/review-hardening

Conversation

@jicowan

@jicowan jicowan commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes every confirmed finding from the three-way security / performance / scalability review (PRD-68). Six phases, one commit each, plus a migration-script fix found during deploy.

Security

  • Manifest YAML injection: yamlQuote emits a fully escaped scalar; every user-derived template sink goes through it; typed decode uses DisallowUnknownFields; generated pods set allowPrivilegeEscalation: false. New internal/api/validate.go rejects malformed model ids, S3 URIs, tags, tokens, enums (400).
  • Per-object ownership (created_by, migration 045): cancel/delete are owner-or-admin. Helm refuses cognito.authDisabled=true on an internet-facing ingress. Per-IP rate limit on /auth/*. 35 handlers stop returning raw err.Error() on 5xx.

Lifecycle / scalability

  • Per-pod admission semaphore (MAX_CONCURRENT_RUNS, default 4); queued runs stay pending and cancellable. Distributed runs wait for the pool lock instead of failing.
  • One coordination poller per pod cancels runs that were cancelled, deleted, re-claimed by another pod (fenced), or already finalized. Terminal writes are conditional (ErrRunNotActive). owner_pod set at insert.
  • Cross-pod DELETE now sets the DB cancel flag, so the owner tears the workload down (previously orphaned it).
  • Graceful SIGTERM drain (SHUTDOWN_GRACE 10m, Helm grace 660s, preStop, PDB, /readyz, /metrics). A rolling restart no longer kills in-flight runs.
  • Advisory locks for orphan recovery, catalog MV refresh, model-cache adoption, leak reconciler, retention. Orphans salvaged from S3 when the loadgen finished. Model-cache jobs owned + recovered.
  • OOM detection label fix (app=app.kubernetes.io/name=, was dead code), client-go QPS 50/100, pgxpool sizing + statement_timeout.
  • 12-char resource names + accelbench/run-id labels + leak reconciler; the suite suite-run-id label is now actually set (was selected on but never emitted).
  • migrate.sh keeps a schema_migrations ledger (lenient bootstrap for pre-ledger DBs, strict thereafter). Karpenter GPU/Neuron/memory limits + opt-in Cognito threat protection in Terraform (not applied). Opt-in RUN_RETENTION_DAYS purge.

Performance

  • Model-config (10m) / HF-token (5m) / calibration (60s) caches; shared S3 client; ListJobs page+count batch; dashboard per-day series server-side; GetBenchmarkRun joins names; suite overrides loaded once; response cache sweep + cap; /suite-runs paginated.
  • React.lazy routes: initial chunk 893 KB → 215 KB. Dashboard fetches 8 runs + 3 suites; point lookups for cached models; status poll pauses in hidden tabs.

Live validation (accelbench-eks, 2026-09-02)

Check Result
Admission (cap=1 for the test) 3 submits → 1 active, 2 queued
Cross-pod DELETE (queued + running) 204; owner cancelled in <5s; Deployment/Job/ConfigMap torn down; slot freed
Rolling restart mid-run SIGTERM 22:50:14 → HTTP closed 22:50:25 → run completed 22:50:29, 600/600 requests persisted
Leaked resources after 5 deletes + restart none
/readyz /metrics, region validation, lazy bundle all as designed

GPU path not re-validated (g5/g6 ICE in all us-east-2 AZs during the window); used the r8g CPU tier. Pre-existing and out of scope: CPU-tier runs persist NULL total_cost_usd; RDS secret rotation is enabled again after the Aurora rebuild.

Test plan

  • go test ./... && go vet ./...
  • cd frontend && npx tsc --noEmit && npm run build && npx vitest run
  • helm lint + fail-guard render check; terraform validate
  • Deployed + live-validated (table above)

🤖 Generated with Claude Code

jicowan and others added 8 commits September 2, 2026 16:25
…68 P1)

- yamlQuote now emits a single fully-escaped YAML scalar (single-quoted for
  plain-printable inline JSON, otherwise JSON-encoded); toYAMLStringList
  JSON-encodes elements. Every user-derived template sink (HF token, model
  id, S3 URI, kv-cache dtype, revision, bucket, prefix, region, cache id)
  goes through it in all six templates.
- Generated model pods always set allowPrivilegeEscalation=false and drop
  the SA token unless the streamer SA is needed.
- New internal/api/validate.go: strict charset/enum validation for every
  free-text field on run / suite / model-cache / register requests, wired
  into the four create paths (400 with a field-specific message).
- Typed manifest decode uses DisallowUnknownFields (orchestrator applyYAML
  and cache-job apply).
- Tests: validator table tests + a render test proving a crafted hf_token
  cannot add securityContext.privileged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
…, scraper cost (PRD-68 P2)

- waitForReady listed pods with "app=<name>" but the template labels
  app.kubernetes.io/name — the OOM scan matched nothing, so every OOM
  burned the full 25-minute readiness timeout. Fixed the selector.
- oom.Detector gains CheckPodObject: readiness loops pass the pod they
  already listed (no Pods.Get per pod per tick) and the Events.List is
  only issued when a container has terminated or restarted.
- client-go QPS/Burst raised from the 5/10 default to 50/100
  (K8S_CLIENT_QPS / K8S_CLIENT_BURST) so a dozen concurrent runs no
  longer client-side-throttle teardown behind readiness polls.
- pgxpool configured explicitly: MaxConns 10 / MinConns 2 (DB_MAX_CONNS,
  DB_MIN_CONNS), idle/lifetime bounds, statement_timeout 30s
  (DB_STATEMENT_TIMEOUT_SECONDS), application_name.
- Host-memory scraper samples every 15s instead of 5s and caches the
  pod-name set instead of a Pods.List per sample (re-resolves on roll).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
…cing poller, cross-pod delete (PRD-68 P3)

Admission: Execute/ExecuteSuite take a per-pod slot (MAX_CONCURRENT_RUNS,
default 4) after registering for cancel and before touching the cluster;
queued runs stay pending and cancellable. A catalog seed no longer fans the
whole matrix out to Karpenter at once. Distributed runs wait for the shared
pool lock (DISTRIBUTED_LOCK_WAIT, default 30m) instead of failing.

Coordination: one poller per pod reads owner_pod/cancel_requested/status
for every owned run in a single query every 5s and cancels runs that were
cancelled, DELETED, re-claimed by another pod (fenced), or already moved to
a terminal status by recovery. Replaces the per-run pollers. Terminal
status writes are conditional (WHERE status IN pending/running) and return
ErrRunNotActive so a stale owner can never overwrite recovery's outcome.
Runs and suites are inserted with owner_pod set.

Delete: an active run/suite delete now sets the DB cancel flag before
removing the row; the owner's poller treats the missing row as cancel and
drives normal teardown (previously a cross-pod delete orphaned the model
Deployment + loadgen Job until natural completion).

Recovery: orphan recovery, catalog MV refresh run under a Postgres advisory
lock so one replica does the work; orphaned runs are salvaged from the S3
summary (completed + cost) when the loadgen already finished, else failed.

Shutdown: SIGTERM -> http.Server.Shutdown, then Drain waits up to
SHUTDOWN_GRACE (10m) for in-flight runs while heartbeat + poller keep
running; Helm sets terminationGracePeriodSeconds 660, a preStop sleep,
readiness on /readyz (DB ping + not draining), and a PDB (minAvailable 1).
/metrics exposes run/queue/recovery/fencing gauges in Prometheus text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
- Migration 045: benchmark_runs.created_by / test_suite_runs.created_by
  (Cognito sub of the submitter), model_cache.owner_pod, and partial
  indexes for the active-owner orphan scans + created_by.
- Cancel and delete of runs/suites are owner-or-admin (reads stay open to
  every role per PRD-48; legacy NULL created_by rows stay mutable).
- Model-cache rows are owned by the watching pod; a 60s-grace/30s recovery
  loop (advisory-locked) adopts 'caching' rows whose pod stopped
  heartbeating, re-attaches a watcher, and fails rows whose Job is gone.
  Previously a pod restart mid-cache stranded the row in 'caching' forever
  and blocked re-caching via the 409.
- /model-cache gains ?hf_id= for point lookups (used by P5 frontend work).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
…utes (PRD-68 P5)

Backend
- FetchModelConfig serves repeats from a 10m in-process cache; the platform
  HF token is cached 5m in secrets.Manager (invalidated on write); the
  host-memory calibration aggregate is cached 60s. /recommend no longer
  costs an S3/HF round-trip + Secrets Manager call + full-table percentile
  per slider tick.
- One shared S3 client (recommend.SharedS3Client) replaces
  LoadDefaultConfig-per-call in the recommender and the results reader.
- ListJobs: page + COUNT as two statements in one pgx batch instead of
  COUNT(*) OVER () on the materialized UNION, so the created_at indexes
  are usable again. ListSuiteRunsWithNames is paginated {rows,total}
  (was a bare array hard-capped at 100).
- DashboardStats: counts + lifetime cost in one pass; the 14-day series
  now carries runs/suites per day so the Dashboard chart needs no list
  fetch; the per-day scan is bounded to the last 14 days.
- GetBenchmarkRun joins model/instance names so the polled run detail is
  one query; suite detail loads scenario overrides once (was N+1).
- Response cache sweeps expired entries every 60s and caps at 10k; the
  pricing region key is validated against an AWS region pattern.
- Orphan reap short-circuits with one Nodes.List when no multinode nodes
  exist (was NodePools.List + per-pool EC2NodeClass.Get + Nodes.List every 30s).

Frontend
- React.lazy for every route except Login/Dashboard: initial chunk
  893 KB -> 215 KB.
- Dashboard fetches 8 runs + 3 suites (was 100 + all); Run page and combobox
  use ?hf_id=/status= point lookups; status poll pauses in hidden tabs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
…rd, Karpenter limits, retention, auth rate limit (PRD-68 P6)

- Resource names embed 12 chars of the run id (was 8 → 32-bit collision
  space); every generated Deployment/Service/Job/ConfigMap carries
  accelbench/run-id. The suite loadgen Jobs now actually get the
  suite-run-id label CleanupSuiteResources selects on (it was never set,
  so suite loadgen cleanup matched nothing).
- Leak reconciler (5m, advisory-locked): deletes accelbench/role objects
  whose run row is terminal or gone after a 10m grace; legacy unlabelled
  objects only once older than any possible run.
- docker/migrate.sh keeps a schema_migrations ledger, skips applied files,
  and runs psql with ON_ERROR_STOP so a failing statement fails the Job.
- Helm: ingress.scheme (internet-facing|internal); the chart refuses to
  render cognito.authDisabled=true on an internet-facing ALB unless
  ingress.allowAuthDisabled=true. api.runRetentionDays wires
  RUN_RETENTION_DAYS.
- Terraform (NOT applied — plan first): nvidia.com/gpu, aws.amazon.com/neuron
  and memory limits on the gpu/neuron/cpu NodePools (variables with
  defaults 64/64/8000Gi); opt-in cognito_advanced_security_mode.
- Opt-in retention: RUN_RETENTION_DAYS purges terminal runs/suites (and
  metrics) older than N days hourly in batches; default 0 = off.
- Per-IP token-bucket limiter (30/min, burst 10) on the public /auth/*
  routes; 429 + Retry-After.
- 35 handler sites that returned raw err.Error() on 5xx now log the detail
  and return an opaque message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
… P6)

001_initial.sql uses bare CREATE TABLE, so the first strict replay against
an existing database failed. When the ledger is empty and benchmark_runs
exists, replay once the old lenient way and back-fill schema_migrations;
every later run is strict (ON_ERROR_STOP). Verified live on accelbench-eks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
Comment thread internal/database/repository.go Fixed
Comment thread internal/database/repository.go Fixed
…eQL)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
Comment thread internal/database/repository.go Fixed
Comment thread internal/database/repository.go Fixed
…ger-conversion)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Jeremy Cowan <jicowan@hotmail.com>
@jicowan
jicowan merged commit 69ed169 into main Sep 3, 2026
7 checks passed
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.

2 participants