Skip to content

Migration PR 4, Stages 2+3: capacity qualification record and production Cloud Run configuration#3741

Merged
anth-volk merged 8 commits into
masterfrom
feat/migration-pr4-stage2-runtime-timing
Jul 7, 2026
Merged

Migration PR 4, Stages 2+3: capacity qualification record and production Cloud Run configuration#3741
anth-volk merged 8 commits into
masterfrom
feat/migration-pr4-stage2-runtime-timing

Conversation

@anth-volk

@anth-volk anth-volk commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two stages of the Cloud Run host-cutover plan in one reviewable arc: the Stage 2 measurement campaign's tooling and evidence (commits 1–2), the Stage 3 production configuration that encodes its results (commits 3–6), and a self-review fixes commit (7).

Commits

  1. Load generator (scripts/measure_cloud_run_runtime.py) — closed-loop asyncio/httpx clients with cache-busting via a top-level nonce (/us/calculate is Redis-cached on the full body hash; unbusted load tests measure Redis at ~260ms instead of the engine at ~44s+). Reusable for the Stage 10 ramp watchpoints.
  2. Stage 2 record + 16 raw cell JSONs (docs/migration/history/) — cold-start distribution (n=18 mined + 282.8s live user-facing wake), traffic profile (peak 10.98 RPS, mostly light/cached), warm parity (per-probe PASS after Lower ASGI gzip compresslevel from 9 to 4 #3735 — Cloud Run beats App Engine on every probe), the worker/concurrency cells, and both soaks (c8: OOM SIGKILL at 92% memory; c4: clean at 69%).
  3. Runtime-shape pins: --concurrency 4 + WEB_CONCURRENCY=2 explicit on every deploy (both services — staging mirrors production's shape). Explicit pinning is non-negotiable after the campaign experienced template pollution (a mid-campaign CI deploy inherited a test concurrency) and the --concurrency default=platform-640 trap.
  4. Per-job scaling pins in push.yml: production max-instances 4; staging min 0 / max 1. Revision-level min-instances stays 0 everywhere — warm capacity is service-level only (revision-level would keep a warm 16Gi instance per accumulated stage3-* tag).
  5. Invariants: dry-run tests enforcing the pins (boundary-anchored) and forbidding a nonzero revision-level min on the production job (32/32 pass).
  6. Operations doc: runtime-shape section + the one-time manual service-level --min-instances 1 runbook with WHEN-to-run and verification steps.
  7. Self-review fixes — most notably: gunicorn worker recycling (--max-requests), originally included, was removed after adversarial review verified it was unsafe as configured (the request counter advances on cheap requests, so at peak a worker would recycle every ~90–110s against a ~170s+ re-import that runs CPU-throttled without boost; jitter 100 cannot decorrelate two workers; and the qualifying soak never executed a recycle). It returns only with its own qualification — rationale recorded in the ops doc and the Stage 2 record's follow-ups. Also: docs-drift fixes (source-of-truth layering, measured cold-start figures), changelog honesty about staging, comment dedup, load-generator cleanup.

Verification

  • 32/32 deploy invariants + 15/15 asgi factory tests; shellcheck/bash -n clean; actionlint: no new findings; ruff clean.
  • All Stage 2 measurements against tagged no-traffic revisions; serving traffic verified untouched throughout; all variant tags/revisions cleaned up; service template verified restored.

After merge

  1. One-time manual: gcloud run services update policyengine-api --min-instances 1 (service-level; runbook with verification in the ops doc).
  2. Stage 3 exit gates on the next push cycle: new revisions show containerConcurrency: 4 / maxScale: 4; after 30+ min idle, /readiness-check TTFB < 2s; billing shows exactly one warm instance.

Refs: Stage 2 section of the cutover execution plan (planning folder); #3734/#3735 (gzip parity fix found by Phase A).


🤖 Generated with Claude Code

anth-volk and others added 5 commits July 7, 2026 21:00
Committed locally on the Stage 2 branch; pushed with the Stage 3 PR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full lean-protocol campaign: cold-start distribution, traffic profile,
warm parity (post-gzip-fix, per-probe PASS), worker/concurrency cells,
and soaks. Decision: WEB_CONCURRENCY=2, --concurrency 4, service-level
min-instances 1, max-instances 4, memory unchanged at 16Gi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--concurrency 4 and WEB_CONCURRENCY=2 per the capacity campaign: two
workers beat one decisively (GIL-bound engine), and admission control
at 4 is what keeps the two-worker instance memory-safe in 16Gi (the
concurrency-8 soak OOM-killed a worker at 92% memory; concurrency 4
soaked clean at 69%). Pinned explicitly on every deploy because gcloud
inherits unspecified template fields — a mid-campaign CI deploy shipped
candidates with an inherited test concurrency — and because
--concurrency default resolves to the platform default (640), not the
historical 80.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Production capacity is an explicit workflow decision, not a script
default: max-instances 4 sized from the Stage 2 campaign (peak real
traffic ~11 RPS, per-instance ceiling ~1-2 concurrent uncached
calculates). Staging pins min 0 / max 1 explicitly. Revision-level
min-instances stays 0 everywhere; warm capacity is set service-level,
once, outside CI (revision-level would warm one 16Gi instance per
accumulated revision tag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.89%. Comparing base (4407075) to head (9d8bc7b).
⚠️ Report is 12 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3741      +/-   ##
==========================================
+ Coverage   79.82%   79.89%   +0.07%     
==========================================
  Files          70       70              
  Lines        4336     4332       -4     
  Branches      808      807       -1     
==========================================
  Hits         3461     3461              
+ Misses        655      652       -3     
+ Partials      220      219       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

anth-volk and others added 2 commits July 7, 2026 21:55
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Worker recycling (--max-requests) is removed rather than retuned:
review analysis showed the 500-request budget recycles faster than the
~170s re-import at peak (the counter advances on cheap requests too),
jitter 100 cannot decorrelate two workers, a mid-life re-import runs
without cpu-boost and CPU-throttled at idle, and the qualifying soak
never executed a recycle. The ops doc now records why it needs its own
qualification. Also: pin assertions get boundary anchors; the ops doc's
source-of-truth section acknowledges push.yml job pins; stale cold-start
figures replaced with measured values; the changelog credits staging's
shape change; the min-instances runbook gains WHEN + verification; the
duplicated rationale comments collapse to pointers; load-generator
cleanup (single exception path, Counter, always/never cache-bust, dead
fields removed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@anth-volk anth-volk force-pushed the feat/migration-pr4-stage2-runtime-timing branch from 22e389f to 041a882 Compare July 7, 2026 20:08
…d percentile

- The scaling/runtime-shape invariants now scan the whole workflow file:
  any mention of the min-instances variable must be an explicit zero pin
  (workflow-level env flows into every job), and the concurrency/worker
  variables may not appear in the workflow at all — their only home is
  cloud_run_env.sh, which the dry-run test validates.
- The load generator's cache-bust nonce gains a per-run token so
  re-running a cell within Redis's 300s TTL cannot replay identical
  body hashes and measure the cache.
- The warm-up calculate is best-effort: a timeout no longer loses the
  cell after the readiness wait.
- _percentile is shared from capture_migration_baseline (one definition
  of p95 across the evidence set) and fixed to round half up — banker's
  rounding biased small even samples low (p50 of two values picked the
  minimum).
- achieved_rps divides by the measured window rather than the configured
  duration (in-flight requests at the deadline run to completion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@anth-volk anth-volk marked this pull request as ready for review July 7, 2026 20:46
@anth-volk anth-volk merged commit 56cb6a3 into master Jul 7, 2026
10 checks passed
@anth-volk anth-volk deleted the feat/migration-pr4-stage2-runtime-timing branch July 7, 2026 20:55
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.

1 participant