Skip to content

Prewarm front backend before GCP cutover - #164

Merged
lawrencecchen merged 9 commits into
mainfrom
fix/golden-canary-cutover
Aug 5, 2026
Merged

lawrencecchen merged 9 commits into
mainfrom
fix/golden-canary-cutover

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

Starts held Codex sessions before migration preparation, keeps the front backend referenced through an unadvertised canary host, and requires five minutes of public canary plus backend-health evidence before active route changes. Also fixes RSS sampler waiting in a subshell.

Tests: go test -p 1 ./...; focused URL-map and golden harness regressions; shell syntax and shellcheck.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Prewarms the front backend through a hidden, authenticated canary route, hard-binds routing selectors to the target instance, and continuously samples front readiness (public canary + backend health) before switching GCP URL‑map routes. Requires five minutes of uninterrupted evidence before changing the active route.

  • New Features

    • Added url-map-routing.py to keep a dedicated canary host/matcher, switch only the active route, retain the warm canary across switches, and assert URL‑map state.
    • Added canary-security-policy.py to render/verify a Cloud Armor policy that authenticates the canary via a token; migration scripts apply and pin this policy per instance.
    • Added wait-for-front-readiness.py and probe-front-readiness.sh to sample public canary responses tied to backend health; the probe forwards the canary token; shared parsing moved to backend_health.py.
    • Extended evidence with routing.active_matcher and routing.canary (host, security policy, attempts, sessions, density, timings); validators pin matcher names, canary host, and security policy per instance and ensure routing.canary.backend_url equals the front URL; migration scripts assert selectors, require a held Codex session before prepare, and run the continuous readiness probe during prewarm.
  • Bug Fixes

    • Reject retargeted selectors, wrong/smuggled/forged canary proofs, missing/partial or gapped canary evidence, and mismatched URL‑map reference counts.
    • Tie backend‑health samples to successful public canary responses.

Written for commit 456b008. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added safer canary routing controls for staged migrations, including preparation, active-route switching, rollback, and route-state verification.
    • Added validation for canary hostnames, backend health, session continuity, routing evidence, and minimum stability duration.
    • Added support for instance-specific staging and production migration targets.
  • Bug Fixes

    • Prevented invalid, incomplete, or hijacked canary routing from being accepted.
    • Improved migration checks to ensure legacy routes remain correctly controlled during cutover.
  • Tests

    • Expanded end-to-end coverage for canary preparation, cutover, rollback, continuity, and invalid routing scenarios.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds a URL-map routing helper, front-slot canary preparation, evidence validation, migration-script integration, and regression coverage for routing state, timing, session continuity, and invalid canary proofs.

Changes

Front-slot migration

Layer / File(s) Summary
URL-map routing helper
deploy/gcp/url-map-routing.py
Adds constrained URL-map parsing, canary preparation, active-route rewriting, state assertions, atomic writes, and CLI error handling.
Canary preparation and evidence validation
deploy/gcp/migrate-to-front-slots.sh, deploy/gcp/validate-deploy-evidence.py
The migration creates and probes an unadvertised canary route. Evidence records routing identifiers, timestamps, attempts, session hashes, and stability duration. Validators enforce instance-specific routing and timing rules.
Cutover and retirement routing integration
deploy/gcp/switch-front-migration.sh, deploy/gcp/finalize-legacy-retirement.sh, deploy/gcp/golden-local-mac-production-continuity.sh
Migration transitions and retirement checks use the routing helper. The continuity script exports a normalized cloud configuration path. RSS sampler results use caller-provided variables.
Observer fixtures and regression coverage
cmd/subrouter-transport-observer/*, cmd/subrouter/deploy_scripts_test.go
Observer validation captures canary evidence and runs after session setup. Fixtures and tests cover valid, fractional-millisecond, missing, short-duration, incorrect-host, idempotent, cutover, rollback, and route-preservation cases.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Migration as migrate-to-front-slots.sh
  participant Routing as url-map-routing.py
  participant Front as Front backend
  participant Journal as Front-slot journal
  participant Validator as validate-deploy-evidence.py
  Migration->>Routing: Prepare and assert canary route
  Migration->>Front: Send authenticated public canary requests
  Front->>Journal: Record front-slot request
  Migration->>Validator: Emit routing and stability evidence
  Validator-->>Migration: Validate canary and backend-health timing
Loading

Possibly related PRs

  • manaflow-ai/subrouter#155: Modifies the same front-migration orchestration script for retry convergence and stale-topology reconciliation.
  • manaflow-ai/subrouter#160: Modifies the same migration evidence validators, scripts, and regression tests for canary timing and bounded migration behavior.
  • manaflow-ai/subrouter#163: Extends the same backend-health validation, fixtures, scripts, and regression tests with canary-routing requirements.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: prewarming the front backend before GCP cutover.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/golden-canary-cutover

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 7

🧹 Nitpick comments (5)
cmd/subrouter/deploy_scripts_test.go (1)

21-145: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strong end-to-end coverage of the routing helper.

Every destination path uses a fresh t.TempDir(), so no call violates the source-differs-from-destination rule in write_map. The hijacked-host case on line 141 rejects for the correct reason: the canary matcher is still present while the expected host is gone, so canary_state fails the partial-configuration check on line 233 of deploy/gcp/url-map-routing.py.

One gap. No case uses backend URLs where one is a prefix of another. That is the scenario behind the substring reference counting raised on deploy/gcp/url-map-routing.py lines 263-272. Add a case with URLs such as .../staging-front and .../staging-front-v2 when that fix lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/subrouter/deploy_scripts_test.go` around lines 21 - 145, Extend
TestGCPURLMapCanaryRemainsReferencedAcrossActiveRouteSwitches with a backend URL
prefix-collision case, using distinct URLs such as staging-front and
staging-front-v2. Exercise the relevant prepare/rewrite/assert flow and verify
references are counted per complete URL rather than by substring, ensuring the
shorter URL is not mistaken for a reference to the longer one.
deploy/gcp/url-map-routing.py (1)

339-359: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider named options for the shared arguments.

Three subcommands take six to eight positional arguments in a fixed order. A call site that swaps canary_host and canary_url fails closed on the HOST pattern, so the current risk is low. Named options such as --active-matcher and --canary-host would make the call sites in migrate-to-front-slots.sh, switch-front-migration.sh, and finalize-legacy-retirement.sh self-describing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/gcp/url-map-routing.py` around lines 339 - 359, Update build_parser to
replace the shared positional arguments for prepare-canary, rewrite-active, and
assert-state with explicit named options, including --active-matcher,
--expected-active-url, --canary-matcher, --canary-host, and --canary-url, while
preserving command-specific source, destination, new_url, and --forbid-url
behavior. Update the three shell call sites to pass these values using the new
option names.
cmd/subrouter-transport-observer/testdata/golden_fake_client/main.go (1)

59-66: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Count only registered PID records, not temporary files.

registerFakeProcess on lines 444-448 creates a .process-* temporary file in the same directory before renaming it to the PID path. os.ReadDir counts that temporary file. A session registration that overlaps this check can therefore push the count past 2 without a held session being registered yet, which weakens the guard the test is meant to enforce.

The current test starts several sessions before preparation, so the count is far above the threshold and the test is not flaky. Filtering the prefix makes the assertion match its stated intent.

♻️ Proposed refactor
 	if operation == "migration-prepare" && os.Getenv("SUBROUTER_GOLDEN_FAKE_REQUIRE_SESSIONS_DURING_PREPARE") == "1" {
 		entries, err := os.ReadDir(os.Getenv("SUBROUTER_GOLDEN_FAKE_PROCESS_STATE"))
 		// The harness process and this action are already registered. Require at
 		// least one separately held Codex session before migration preparation.
-		if err != nil || len(entries) <= 2 {
+		registered := 0
+		for _, entry := range entries {
+			if !strings.HasPrefix(entry.Name(), ".process-") {
+				registered++
+			}
+		}
+		if err != nil || registered <= 2 {
 			os.Exit(9)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/subrouter-transport-observer/testdata/golden_fake_client/main.go` around
lines 59 - 66, Update the migration-prepare guard in the operation check to
count only registered PID record entries, filtering the os.ReadDir results to
the established PID filename pattern and excluding temporary .process-* files
before applying the threshold. Preserve the existing error and exit behavior
while ensuring the session-count assertion reflects registered processes only.
deploy/gcp/migrate-to-front-slots.sh (1)

193-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Distinguish an SSH failure from a missing journal entry.

canary_session_observed sends both stdout and stderr to /dev/null. A broken IAP tunnel, an expired credential, and a missing journal entry all return the same non-zero status. The caller then retries for ten minutes and fails with "did not correlate through the public load balancer", which points the operator at the load balancer instead of the SSH path. The behavior is fail-closed, so this is a diagnosability gap only.

Record the SSH stderr to an artifact file so the operator can separate the two causes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deploy/gcp/migrate-to-front-slots.sh` around lines 193 - 200, Update
canary_session_observed to preserve the gcloud SSH command’s stderr in an
artifact file instead of discarding it with >/dev/null 2>&1. Keep journal-query
stdout suppressed and retain the existing exit-status behavior, ensuring the
artifact captures IAP, credential, or SSH failures for diagnosis.
cmd/subrouter-transport-observer/golden_acceptance_regression_test.go (1)

179-179: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for the twenty-minute canary cap.

Three new rules have no negative case: the unsupported instance on line 271 of cmd/subrouter-transport-observer/golden_migration_evidence.go, the 600-attempt ceiling on line 289, and the twenty-minute canary cap on line 340. The twenty-minute cap matters most, because deploy/gcp/migrate-to-front-slots.sh can produce a longer window without failing. A negative case documents the boundary that the script must respect.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/subrouter-transport-observer/golden_acceptance_regression_test.go` at
line 179, Add a negative acceptance test in the relevant golden regression test
cases covering the twenty-minute canary cap defined by the migration evidence
rules. Exercise a canary window longer than twenty minutes and assert that it is
rejected, using the existing test structure and symbols around the canary-cap
rule rather than changing the production script.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/subrouter-transport-observer/golden_acceptance_regression_test.go`:
- Around line 160-169: Update the shortCanary fixture by moving
first_observed_at forward to create a sub-five-minute canary window, while
keeping canary verified_at at 2026-08-02T00:05:00Z and preserving the valid
ordering chain. Leave the validateGo and validatePython assertions focused
solely on rejecting the duration bound.

In `@deploy/gcp/migrate-to-front-slots.sh`:
- Around line 427-431: Update the canary stability validation near
canary_stable_duration_ms and the existing five-minute lower-bound check to also
reject durations above the validator’s 20-minute maximum (1,200,000 ms). Ensure
the script fails with a clear diagnostic before writing evidence, while
preserving acceptance of durations within the inclusive five-to-twenty-minute
range.

In `@deploy/gcp/switch-front-migration.sh`:
- Around line 115-117: The routing extraction in
deploy/gcp/switch-front-migration.sh lines 115-117 must default missing jq
fields to empty and immediately die when ACTIVE_MATCHER, CANARY_MATCHER, or
CANARY_HOST is empty. Apply the same change to
deploy/gcp/finalize-legacy-retirement.sh lines 73-75 for its corresponding
variables, preserving the existing validation flow.

In `@deploy/gcp/url-map-routing.py`:
- Around line 146-155: Update the host parsing in host_rule to validate every
non-field line in the item, rather than only the slice between hosts and
pathMatcher. Reject any host entry appearing after pathMatcher (and preserve
rejection of unsupported YAML), while continuing to collect valid hosts and
enforce non-empty uniqueness.
- Around line 263-272: Replace raw body.count(url) checks in the
reference-validation loops with counts derived from parsed defaultService
values, comparing complete service URLs rather than substrings. Update both
expected reference validation and forbidden URL validation while preserving the
existing failure messages and distinct-backend checks.

In `@deploy/gcp/validate-deploy-evidence.py`:
- Around line 656-661: Require the two canary session hashes to be distinct in
both validators. In deploy/gcp/validate-deploy-evidence.py lines 656-661,
capture the results of the sha() calls for first_session_sha256 and
verified_session_sha256 and fail when they match; in
cmd/subrouter-transport-observer/golden_migration_evidence.go lines 286-293, add
equality of canary.FirstSessionSHA256 and canary.VerifiedSessionSHA256 to the
migration_canary_invalid condition.
- Around line 619-634: Align the instance handling in the validator around
expected_routing with migrate-to-front-slots.sh and
golden_migration_evidence.go: support custom instances by deriving the active
matcher, canary matcher, and canary host from the declared environment variables
instead of rejecting instances outside the fixed table, while retaining the
named-instance defaults.

---

Nitpick comments:
In `@cmd/subrouter-transport-observer/golden_acceptance_regression_test.go`:
- Line 179: Add a negative acceptance test in the relevant golden regression
test cases covering the twenty-minute canary cap defined by the migration
evidence rules. Exercise a canary window longer than twenty minutes and assert
that it is rejected, using the existing test structure and symbols around the
canary-cap rule rather than changing the production script.

In `@cmd/subrouter-transport-observer/testdata/golden_fake_client/main.go`:
- Around line 59-66: Update the migration-prepare guard in the operation check
to count only registered PID record entries, filtering the os.ReadDir results to
the established PID filename pattern and excluding temporary .process-* files
before applying the threshold. Preserve the existing error and exit behavior
while ensuring the session-count assertion reflects registered processes only.

In `@cmd/subrouter/deploy_scripts_test.go`:
- Around line 21-145: Extend
TestGCPURLMapCanaryRemainsReferencedAcrossActiveRouteSwitches with a backend URL
prefix-collision case, using distinct URLs such as staging-front and
staging-front-v2. Exercise the relevant prepare/rewrite/assert flow and verify
references are counted per complete URL rather than by substring, ensuring the
shorter URL is not mistaken for a reference to the longer one.

In `@deploy/gcp/migrate-to-front-slots.sh`:
- Around line 193-200: Update canary_session_observed to preserve the gcloud SSH
command’s stderr in an artifact file instead of discarding it with >/dev/null
2>&1. Keep journal-query stdout suppressed and retain the existing exit-status
behavior, ensuring the artifact captures IAP, credential, or SSH failures for
diagnosis.

In `@deploy/gcp/url-map-routing.py`:
- Around line 339-359: Update build_parser to replace the shared positional
arguments for prepare-canary, rewrite-active, and assert-state with explicit
named options, including --active-matcher, --expected-active-url,
--canary-matcher, --canary-host, and --canary-url, while preserving
command-specific source, destination, new_url, and --forbid-url behavior. Update
the three shell call sites to pass these values using the new option names.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a48aef3-b231-4297-b5da-6d91b2e9cb13

📥 Commits

Reviewing files that changed from the base of the PR and between 12408ad and e2b0ad1.

📒 Files selected for processing (12)
  • cmd/subrouter-transport-observer/golden_acceptance_regression_test.go
  • cmd/subrouter-transport-observer/golden_local_mac_harness_test.go
  • cmd/subrouter-transport-observer/golden_migration_cycle.go
  • cmd/subrouter-transport-observer/golden_migration_evidence.go
  • cmd/subrouter-transport-observer/testdata/golden_fake_client/main.go
  • cmd/subrouter/deploy_scripts_test.go
  • deploy/gcp/finalize-legacy-retirement.sh
  • deploy/gcp/golden-local-mac-production-continuity.sh
  • deploy/gcp/migrate-to-front-slots.sh
  • deploy/gcp/switch-front-migration.sh
  • deploy/gcp/url-map-routing.py
  • deploy/gcp/validate-deploy-evidence.py

Comment thread deploy/gcp/migrate-to-front-slots.sh Outdated
Comment thread deploy/gcp/switch-front-migration.sh Outdated
Comment thread deploy/gcp/url-map-routing.py
Comment thread deploy/gcp/url-map-routing.py
Comment thread deploy/gcp/validate-deploy-evidence.py Outdated
Comment thread deploy/gcp/validate-deploy-evidence.py Outdated
@lawrencecchen
lawrencecchen merged commit 289b12d into main Aug 5, 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.

1 participant