Add EdgeZero canary routing (rollout_pct flag + FNV-1a bucket assignment) (PR 19)#727
Add EdgeZero canary routing (rollout_pct flag + FNV-1a bucket assignment) (PR 19)#727prk-Jr wants to merge 20 commits into
Conversation
…outes_to_edgezero
When edgezero_enabled=true, reads edgezero_rollout_pct (0-100) from the trusted_server_config store. Routes each request to EdgeZero if fnv1a_bucket(client_ip) < rollout_pct, giving the ops team sticky per-user canary control without a deploy. Absent key defaults to 100 (full rollout, backward compatible with edgezero_enabled=true deployments that predate this PR). Invalid values and read errors default to 0 (all legacy, fail-safe).
Set to "0" so local dev and CI stay on the legacy path by default. Ops changes this in the production config store — no re-deploy required.
Documents the two config store keys, canary progression stages (1% → 10% → 50% → 100%), hold-point durations, pass/fail thresholds, and instant rollback procedure.
…ro-pr19-cutover-canary
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
One low-severity docs/ops note: the canary runbook references routing log lines that are emitted at debug level, so production verification may require a debug/log-level deployment or equivalent observability setup.
The absent-key branch logged at warn on every request when edgezero_enabled=true but edgezero_rollout_pct was unset, flooding logs at production QPS while traffic flowed fine. Demote to debug; the setup procedure and migration runbook remain the operational safety net. The absent-key default stays 100 (backward compat) so an existing edgezero_enabled=true deployment is not silently rolled back.
For rollout_pct 0 (full rollback) and 100 (full cutover) — which together cover most of the canary's lifetime — the routing decision is fixed, yet every request still allocated the client-IP routing key string and ran the FNV hash. Match on the rollout value and only compute the bucket for a partial rollout. Also rename canary_routes_to_edgezero to routes_to_edgezero: the predicate is just `bucket < rollout_pct` and applies at any rollout value, so "canary" read misleadingly at the call site.
The four documented branches (absent -> 100, valid -> parsed, invalid -> 0, read error -> 0) are safety-critical but only the parse step was indirectly covered. ConfigStoreHandle::new(Arc::new(...)) over the public ConfigStore trait is constructible in unit tests, so add an in-memory stub store and pin every branch, including the fail-safe defaults that matter during an incident.
The routing verification log lines are emitted at log::debug!, but the Fastly
logger is capped at Info in production, so verifying the canary via log tailing
needs a debug-level deployment or another observability signal (the runbook now
says so and points at the real-time stats split as the alternative).
Also reconcile the documented log format with the adapter: at the degenerate
rollout values (0 and 100) the bucket is no longer computed, so those lines read
`routing request through {legacy,EdgeZero} path (rollout_pct=N)` without a bucket.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated review:
Review Summary
Reviewed PR #727 against feature/edgezero-pr19-spin-adapter, focusing on the Fastly entry-point canary routing, config-store failure behavior, rollout docs, and CI/review context. I did not find any blocking correctness, security, data-loss, authorization, or severe compatibility issues. I left one P2 inline comment about strengthening coverage for the safety-critical entry-point dispatch matrix.
Findings
P0 / Blockers
None.
P1 / High
None.
P2 / Medium
1 inline comment submitted.
P3 / Low
None.
CI / Existing Reviews
All reported PR checks are passing. Existing inline feedback about absent-key logging, config-store branch tests, degenerate rollout hashing, naming, and debug-level log documentation appears to have been addressed in later commits. Local cargo test-fastly rollout -- --nocapture could not run in this environment because Viceroy failed to satisfy fastly_http_downstream::downstream_bot_analyzed; CI's Fastly job is green.
Pull the rollout-percentage routing matrix out of main() into a pure should_route_to_edgezero(rollout_pct, client_ip) helper so the safety-critical wiring from rollout state to the EdgeZero/legacy choice can be unit-tested directly, not just the parsing/hashing helpers it composes. main() keeps the edgezero_enabled gate and config reads as thin I/O glue. Add tests covering the full dispatch matrix: rollout_pct=0 (always legacy), rollout_pct=100 (always EdgeZero), and partial-rollout bucket hit/miss for both present and absent client IPs.
…er' into feature/edgezero-pr19-cutover-canary
The base branch refactored IpAddr out of main.rs and dropped the import. Merging base in lost the import that should_route_to_edgezero and its tests rely on, breaking the wasm32-wasip1 build. Re-add it.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated review:
Review Summary
Reviewed PR #727 against feature/edgezero-pr19-spin-adapter, focusing on the Fastly canary dispatch, rollout percentage parsing/defaults, config-store failure behavior, tests, local Fastly config, and the migration runbook. I did not find any blocking correctness, security, data-loss, authorization, or severe compatibility issues. I left one P2 inline comment about making the production canary observability path explicit before operators rely on the runbook.
Findings
P0 / Blockers
None.
P1 / High
None.
P2 / Medium
1 inline comment submitted.
P3 / Low
None.
CI / Existing Reviews
All reported PR checks are passing. Existing review feedback about absent-key log volume, degenerate rollout hashing, config-store branch coverage, naming, and debug-level log documentation appears addressed by later commits. Local cargo test-fastly rollout -- --nocapture compiled the changed Fastly test binary but could not execute under the installed Viceroy because it lacks fastly_http_downstream::downstream_bot_analyzed; CI's Fastly test job is green.
The runbook told operators to verify the canary via a Fastly real-time-stats traffic split and by log-searching the `routing request through EdgeZero path` lines, but neither signal is usable in production: the route decision is logged only at `debug!` (`should_route_to_edgezero`) and the Fastly logger is pinned to `Info` (`logging::init_logger`, no runtime override), so those lines never reach the production log endpoint, and no real-time-stats split or `x-edgezero-path` marker exists. State plainly that no production per-branch route signal exists yet, base canary verification on aggregate service metrics tracking the staged `rollout_pct` changes, and scope the debug route-decision log lines to local Viceroy validation. Remove the unsupported real-time-stats fallback and the unachievable "debug-level logging deployment" guidance.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated review:
Review Summary
Reviewed PR #727 against feature/edgezero-pr19-spin-adapter, focusing on the Fastly canary dispatch, rollout percentage parsing/defaults, config-store failure behavior, local Fastly config, tests, and the EdgeZero migration runbook. I did not find any blocking correctness, security, data-loss, authorization, or severe compatibility issues. I left one P2 inline comment about a runbook validation signal that is not actually emitted with the current logger configuration.
Findings
P0 / Blockers
None.
P1 / High
None.
P2 / Medium
1 inline comment submitted.
P3 / Low
None.
CI / Existing Reviews
All reported PR checks are passing. Existing review feedback about absent-key logging, degenerate rollout hashing, config-store branch coverage, dispatch-matrix tests, naming, and production route observability appears addressed in later commits. Local cargo test-fastly rollout -- --nocapture compiled the Fastly test binary but could not execute under the installed Viceroy 0.16.4 because it lacks fastly_http_downstream::downstream_bot_analyzed; CI's Fastly job is green.
…idation The migration runbook pointed operators at the should_route_to_edgezero route-decision lines as a local Viceroy validation signal, but the Fastly logger was pinned to Info — gating echo_stdout too — so those debug! lines were emitted nowhere, even under Viceroy. Add an EDGEZERO_LOG_LEVEL env override (default Info) so local Viceroy runs can raise the level to Debug and surface the route-decision lines. Production Fastly Compute does not surface arbitrary process env vars, so the level stays at the safe Info default there. Resolution is a pure, unit-tested helper. Reword the three runbook references to state the override explicitly instead of implying the lines are visible by default.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated review:
Review Summary
Reviewed PR #727 against feature/edgezero-pr19-spin-adapter, focusing on the Fastly canary routing path, rollout percentage parsing/fallbacks, logger-level changes, tests, local config, and EdgeZero migration documentation. I did not find any blocking correctness, security, data-loss, authorization, or severe compatibility issues. I left one P2 inline comment about stale operational guidance duplicated in the implementation plan.
Findings
P0 / Blockers
None.
P1 / High
None.
P2 / Medium
1 inline comment submitted.
P3 / Low
None.
CI / Existing Reviews
All reported PR checks are passing. Existing automated-review feedback about absent-key log volume, config-store branch coverage, degenerate rollout hashing, dispatch-matrix tests, production route observability, and local Viceroy debug logs appears addressed in later commits. Local cargo test -p trusted-server-adapter-fastly --target wasm32-wasip1 rollout_pct --no-run passed; a native cargo test -p trusted-server-adapter-fastly rollout_pct -- --nocapture attempt failed at link time on Fastly hostcall symbols, which is expected for the Fastly adapter outside the wasm/Viceroy path and not counted as a PR finding.
aram356
left a comment
There was a problem hiding this comment.
Summary
Re-review of the canary routing changes against the PR base (feature/edgezero-pr19-spin-adapter). The prior review's findings on the per-request warn flood, degenerate-rollout short-circuit, predicate rename, read_rollout_pct test coverage, and debug-log observability are all resolved. Two findings remain open (both carried over) and three new minor items are noted inline. CI is green (12/12).
Blocking
🔧 wrench
- Fail-open default for absent
edgezero_rollout_pct: absent key →100(full cutover) is the only failure branch that does not fail to legacy. Recommend defaulting to0. (main.rsabsent branch + doc table; runbook failure-modes table) - Runbook overstates stickiness: "sticky per user" should read "sticky per client IP" — NAT sharing and IP changes break the per-user assumption. (
docs/internal/EDGEZERO_MIGRATION.md:19)
Non-blocking
🤔 thinking
- Two config-store reads per request —
is_edgezero_enabledthenread_rollout_pct; consolidatable into one read. (main.rs:316)
⛏ nitpick
resolve_max_leveldoc says "raises" but the override can also lower the level. (logging.rs:44)
📝 note
- Env-var override safety rests on a Fastly-platform property — must not be copied verbatim into the axum/spin/cloudflare adapters. (
logging.rs:25)
CI Status
- fmt: PASS
- clippy: PASS
- rust tests: PASS
- js tests: PASS
Address PR #727 review findings: - Fail-safe default (blocking): an absent `edgezero_rollout_pct` now resolves to 0 (all legacy), matching every other failure branch (unreadable flag, invalid value, read error). Previously absent meant 100 (full cutover) — the only fail-open branch, where deleting the key triggered an instant full rollout. Updates the read_rollout_pct doc table and test, the runbook failure-modes table, and removes the now-unnecessary setup-ordering precondition and do-not-delete warning. - Runbook stickiness wording (blocking): "sticky per user" -> "sticky per client IP", noting NAT sharing and IP changes can re-bucket a user mid-rollout. - logging doc: the EDGEZERO_LOG_LEVEL override "overrides" the level (it can lower as well as raise it), not just "raises" it. - logging doc: warn that the EDGEZERO_LOG_LEVEL knob is Fastly-only and must not be copied into the axum/spin/cloudflare adapters, where runtime env vars are readable and it would reintroduce a per-request prod debug flood. - Cutover plan: replace the stale monitoring guidance (Fastly log search for route-decision lines) with aggregate-metrics-only verification, mirroring the runbook — production has no per-branch route attribution.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated review:
Review Summary
Reviewed PR #727 against feature/edgezero-pr19-spin-adapter, focusing on the Fastly canary routing implementation, rollout defaults, config-store failure behavior, local Fastly config, migration runbook, implementation plan docs, and CI/review context. I did not find any blocking correctness, security, data-loss, authorization, or severe compatibility issues. I left two P2 inline comments about stale operational guidance that still contradicts the current fail-safe absent-key behavior.
Findings
P0 / Blockers
None.
P1 / High
None.
P2 / Medium
2 inline comments submitted.
P3 / Low
None.
CI / Existing Reviews
All reported PR checks are passing. Existing review feedback about fail-open absent-key behavior, sticky-per-user wording, route-decision observability, logging override wording/scope, and stale monitoring guidance appears addressed in code/runbook and replies; however, some duplicated guidance in fastly.toml and the implementation plan remains stale. Locally, cargo test -p trusted-server-adapter-fastly --target wasm32-wasip1 read_rollout_pct --no-run completed successfully.
Address PR #727 follow-up review: the shipped Viceroy config and the implementation plan still documented the old fail-open semantics. - fastly.toml: absent edgezero_rollout_pct now documented as failing safe to legacy (0); removed the obsolete "set 0 before enabling" precondition. - Cutover plan doc: update the background flow, the read_rollout_pct doc-table snippet, the rustdoc summary, the embedded fastly.toml snippet, the embedded runbook (config table, failure-modes table, do-not-delete warning, setup-order precondition), the sticky-routing note, and the invariants list to match the implemented behavior (absent/invalid/unreadable all fail safe to 0; stickiness is per client IP). Added a pointer to docs/internal/EDGEZERO_MIGRATION.md as the canonical runbook so the duplicated copy cannot silently drift again.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated review:
Review Summary
Reviewed PR #727 against feature/edgezero-pr19-spin-adapter, focusing on the Fastly canary-routing code, fail-safe rollout defaults, logging changes, local config, migration runbook, implementation-plan documentation, CI, and existing review feedback. I did not find any blocking correctness, security, data-loss, authorization, or severe compatibility issues. I left two P2 inline comments on stale plan text that still contradicts the implemented fail-safe and per-client-IP semantics.
Findings
P0 / Blockers
None.
P1 / High
None.
P2 / Medium
2 inline comments submitted.
P3 / Low
None.
CI / Existing Reviews
All reported PR checks are passing. Existing feedback about absent-key fail-open behavior, route-decision observability, logging override wording/scope, and stale monitoring guidance appears addressed in the runtime code, fastly.toml, and the canonical migration runbook; the remaining issues are duplicated/stale text in the implementation-plan document. Locally, cargo test -p trusted-server-adapter-fastly --target wasm32-wasip1 read_rollout_pct --no-run and cargo fmt --all -- --check passed; cd docs && npm run format -- --check could not run because prettier is not installed in this environment, while CI's docs format check is green.
| 0 | ||
| } | ||
| }, | ||
| Ok(None) => 100, |
There was a problem hiding this comment.
Automated review: P2: This snippet still documents the old fail-open absent-key behavior.
The table immediately above says an absent edgezero_rollout_pct returns 0, the implementation now returns 0, and the final invariant says absent/invalid/unreadable must never route traffic to EdgeZero. This embedded code block still says Ok(None) => 100, so anyone reusing the implementation plan would reintroduce the exact fail-open behavior this PR just fixed: deleting or omitting the key would trigger full EdgeZero cutover.
Suggested fix: update this arm to return 0 (and preferably include the same debug log/comment as the real implementation), or mark the whole implementation snippet as historical/superseded so it cannot be copied as current guidance.
| - **No new dependencies** — FNV-1a is inlined, not a crate import. | ||
| - **Fail-safe** — absent, invalid, or unreadable `edgezero_rollout_pct` all produce 0 (all legacy), never 100. A missing or misconfigured key can never accidentally route traffic to EdgeZero; rollout requires an explicit percentage. | ||
| - **No handler or core changes** — all changes are confined to `main.rs` entry-point logic and `fastly.toml`. | ||
| - **Sticky routing** — client IP hash is deterministic; the same user always gets the same path at a given `rollout_pct`, preventing split-session bugs. |
There was a problem hiding this comment.
Automated review: P2: This invariant still overstates stickiness as per-user.
The runtime hashes client_ip, and the canonical runbook now correctly says routing is sticky per client IP, not per user: NAT/CGNAT users can share a bucket, and a user whose IP changes can be re-bucketed and switch paths. This final invariant still promises that the same user always gets the same path and that split-session bugs are prevented, which contradicts the implemented routing key and the fixed runbook wording.
Suggested fix: align this invariant with the runbook, e.g. client IP hash is deterministic; the same client IP gets the same path at a given rollout_pct, but NAT sharing and IP changes can re-bucket users.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Review Summary
🔧 Requesting changes. The runtime fail-safe rollout behavior is much improved and CI is green, but local canary validation still relies on a log-level mechanism that Viceroy/Compute guest code will not actually see. I also left accept-ready suggestions for stale plan snippets and validation commands so the PR artifacts stay safe to reuse.
Additional note folded into the body because the exact insertion point is unchanged context in the diff: max_level is currently applied only to the inner log_fastly::Logger. Please also set the same level on the outer fern::Dispatch (e.g. fern::Dispatch::new().level(max_level)) so production debug! route decisions are filtered before timestamp/format construction, not just dropped by the child logger.
| /// Panics if the Fastly logger cannot be built or if the global logger has already | ||
| /// been set. | ||
| pub(crate) fn init_logger() { | ||
| let configured = std::env::var(LOG_LEVEL_ENV).ok(); |
There was a problem hiding this comment.
🔧 wrench — EDGEZERO_LOG_LEVEL is not visible to Viceroy guest code.
std::env::var(LOG_LEVEL_ENV) reads the guest WASI environment, but fastly compute serve/Viceroy does not pass arbitrary shell environment variables like EDGEZERO_LOG_LEVEL into the Compute guest. That means the runbook command EDGEZERO_LOG_LEVEL=debug fastly compute serve still initializes at Info, so the route-decision debug! lines remain unavailable in the only documented branch-level validation path.
One minimal way to make the local validation path real is to key off the Fastly-provided local hostname and keep production at the safe default:
| let configured = std::env::var(LOG_LEVEL_ENV).ok(); | |
| let configured = std::env::var(LOG_LEVEL_ENV).ok().or_else(|| { | |
| (std::env::var("FASTLY_HOSTNAME").as_deref() == Ok("localhost")) | |
| .then_some("debug".to_owned()) | |
| }); |
| > For local pre-production validation under Viceroy, start the simulator with | ||
| > `EDGEZERO_LOG_LEVEL=debug` (the logger defaults to `Info`, which suppresses | ||
| > these lines) — for example `EDGEZERO_LOG_LEVEL=debug fastly compute serve`. |
There was a problem hiding this comment.
🔧 wrench — make the local log validation instructions match a guest-visible signal.
After changing the logger to use a Viceroy/Compute-provided signal (for example FASTLY_HOSTNAME=localhost) instead of an arbitrary shell env var, this runbook should stop telling operators to set EDGEZERO_LOG_LEVEL, which the guest will not see.
| > For local pre-production validation under Viceroy, start the simulator with | |
| > `EDGEZERO_LOG_LEVEL=debug` (the logger defaults to `Info`, which suppresses | |
| > these lines) — for example `EDGEZERO_LOG_LEVEL=debug fastly compute serve`. | |
| > For local pre-production validation under Viceroy, start the simulator normally. | |
| > Viceroy exposes `FASTLY_HOSTNAME=localhost` to guest code, and the Fastly | |
| > logger raises the route-decision level in that local environment while | |
| > production stays at `Info`. |
| 0 | ||
| } | ||
| }, | ||
| Ok(None) => 100, |
There was a problem hiding this comment.
🔧 wrench — this embedded snippet still has the old fail-open absent-key behavior.
The table above, the runtime code, and the runbook all say an absent edgezero_rollout_pct fails safe to 0; this snippet still returns 100. Because this file is an agent handoff, copying it later would reintroduce the exact full-cutover footgun the PR fixed.
| Ok(None) => 100, | |
| Ok(None) => { | |
| log::debug!( | |
| "edgezero_rollout_pct key absent, defaulting to 0 (legacy path — fail safe)" | |
| ); | |
| 0 | |
| } |
| - **No new dependencies** — FNV-1a is inlined, not a crate import. | ||
| - **Fail-safe** — absent, invalid, or unreadable `edgezero_rollout_pct` all produce 0 (all legacy), never 100. A missing or misconfigured key can never accidentally route traffic to EdgeZero; rollout requires an explicit percentage. | ||
| - **No handler or core changes** — all changes are confined to `main.rs` entry-point logic and `fastly.toml`. | ||
| - **Sticky routing** — client IP hash is deterministic; the same user always gets the same path at a given `rollout_pct`, preventing split-session bugs. |
There was a problem hiding this comment.
🔧 wrench — keep the invariant to per-client-IP stickiness.
The runtime hashes client_ip, not a durable user identifier. NAT/CGNAT users can share a bucket, and a user whose IP changes can re-bucket, so the plan should not promise that the same user always gets the same path.
| - **Sticky routing** — client IP hash is deterministic; the same user always gets the same path at a given `rollout_pct`, preventing split-session bugs. | |
| - **Sticky routing** — client IP hash is deterministic; the same client IP gets the same path at a given `rollout_pct`, but NAT sharing and IP changes can re-bucket users. |
| Expected: no diff. If there is output, run `cargo fmt --all` to fix, then re-check. | ||
|
|
||
| ```bash | ||
| cargo clippy -p trusted-server-adapter-fastly --all-targets --all-features -- -D warnings 2>&1 | tail -20 |
There was a problem hiding this comment.
🔧 wrench — use the target-matched Fastly clippy gate.
This host-targeted clippy command conflicts with CLAUDE.md's Fastly gate. Also, the surrounding | tail/| head validation commands should be removed or wrapped in set -o pipefail, otherwise a failed cargo/npm command can look green because the final pipeline command succeeded.
| cargo clippy -p trusted-server-adapter-fastly --all-targets --all-features -- -D warnings 2>&1 | tail -20 | |
| cargo clippy-fastly |
| - [ ] **Run full test suite** | ||
|
|
||
| ```bash | ||
| cargo test-fastly 2>&1 | tail -20 |
There was a problem hiding this comment.
🔧 wrench — don't pipe validation through tail without pipefail.
As written, a failing test command can still exit successfully if tail succeeds. Keep the command's real exit status visible.
| cargo test-fastly 2>&1 | tail -20 | |
| cargo test-fastly |
|
|
||
| ```bash | ||
| cargo fmt --all -- --check 2>&1 | tail -10 | ||
| cargo clippy -p trusted-server-adapter-fastly --all-targets --all-features -- -D warnings 2>&1 | tail -10 |
There was a problem hiding this comment.
🔧 wrench — final verification should use the Fastly clippy alias and preserve exit status.
This is the PR handoff gate, so it should match the repo's target-specific CI command and not hide failures behind tail.
| cargo clippy -p trusted-server-adapter-fastly --all-targets --all-features -- -D warnings 2>&1 | tail -10 | |
| cargo clippy-fastly |
Summary
edgezero_rollout_pct(0–100) to thetrusted_server_configstore so EdgeZero traffic can be shifted incrementally without a deployclient_ipfor deterministic, sticky per-user bucket assignment (0–99); routing predicate isbucket < rollout_pctrollout_pct = 0is the immediate no-deploy rollbackChanges
crates/trusted-server-adapter-fastly/src/main.rsparse_rollout_pct,fnv1a_bucket,canary_routes_to_edgezero,read_rollout_pct; updatedmain()with canary dispatch; 11 unit tests (pinned FNV-1a vectors, distribution, boundary routing)fastly.tomledgezero_rollout_pct = "0"to local Viceroy config store with ops commentdocs/internal/EDGEZERO_MIGRATION.mdCloses
Closes #500
Test plan
cargo test-fastly— 65 + 956 + 21 + 3 tests greencargo test-axum— 31 tests greencargo clippy-fastly && cargo clippy-axum— no warningscargo fmt --all -- --check— cleancd crates/js/lib && npx vitest run— 291 tests greencd crates/js/lib && npm run format— cleancd docs && npm run format— cleancargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute servewithedgezero_rollout_pct = "1"and"0"(rollback)Checklist
unwrap()in production code — useexpect("should ...")logmacros (notprintln!)