Repository navigation
feat(core): add shared request timing collector and middleware - #389
ChristianPavilonis wants to merge 5 commits into
Conversation
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
Adds a generic per-request timing collector (RequestTimings<N, D>) and opt-in, insert-if-absent attachment middleware to edgezero-core, with a shared runtime scenario in the three WASM adapter contract suites. I found no correctness bugs. I'm requesting changes on two API-shape items that are cheap now but become breaking changes once Trusted Server pins a release tag. Neither touches Trusted Server's call sites at 2791ceb.
Findings
Blocking
- 🔧
TimingErrornot#[non_exhaustive]: inconsistent withEdgeError/KvError/ConfigStoreError, and adding it after the tag is breaking (request_timing.rs:17) - 🔧 Hoist
t0out of the mutex: makeselapsed()infallible and the origin structurally immutable;elapsed()currently fails under cross-thread contention (request_timing.rs:100)
Non-blocking
- 🤔 Generic lifecycle names:
headers_ready_total/request_elapsed/resp_bytesmirror TS internals (request_timing.rs:51) - 🤔 Cloudflare clock semantics: workerd's
performance.now()advances only across I/O, and the contract test runs in Chromium (docs/guide/middleware.md:169) - ♻️ Shared fixture via cross-crate
#[path], never run natively: Cloudflare/Fastly/Spintests/contract.rs - ⛏
RequestTimingslacksDebug;must_usemessage wording;with_excluded_pathsreplace semantics undocumented - ⛏ Guide: show paired type aliases (a generic mismatch silently yields no handle); drop the reference to an internal test file
- ⛏ Spec/plan status lines, unchecked tasks, local paths and "keep the PR draft" text are stale relative to the PR
- 🌱 Contention drop counter for native runtimes; config-driven exclusions; exposing the middleware-created handle to outer layers
Behavioral claims above were checked with throwaway probe tests against 9c03cc59: generic-mismatch silence, elapsed() failing under cross-thread contention, exclusion-list replacement, and no handle on the response.
📌 Out of Scope
- Deployed-provider smoke testing is still unchecked in the test plan. It matters most for Cloudflare, where the local harness doesn't use workerd's clock.
- The Axum adapter has no
tests/contract.rs, so the shared timing scenario doesn't run there.
CI Status
- fmt: PASS
- clippy (
--all-features -D warnings): PASS - tests (
--workspace --all-targets): PASS (core: 484 unit tests, plus thecompile_faildoctest) - feature check (
fastly cloudflare spin): PASS - Spin
wasm32-wasip2check: PASS - GitHub: all 22 checks green, including Cloudflare/Fastly/Spin WASM tests
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
Second pass at 9c03cc59. It covers areas the earlier review didn't reach: registering the middleware from the manifest, how the handle interacts with State<T>, CI coverage of the compile-fail test, how strong the runtime test is, and documentation consistency. I checked each finding against the code or with a throwaway probe. I'm requesting changes so these land together with the two 🔧 items from the earlier review, before the release tag.
Findings
Inline:
- 🤔
edgezero.tomlcan't register the middleware: aconst fnconstructor fixes it (middleware.rs:86) - 🤔
State<RequestTimings<…>>returns a 500 that points atwith_state(docs/guide/middleware.md:226) - 🤔 CI never runs the compile-fail test (request_timing.rs:40)
- 🤔 Overlapping spans on one slot add up past wall-clock time (request_timing.rs:129)
- 🤔 The runtime test can't catch a stopped clock (tests/support/request_timing.rs:37)
- ⛏
span(slot)?in the guide doesn't compile in a handler (docs/guide/middleware.md:233) - ⛏ The
web_timeexample needs aweb-timedependency (docs/guide/middleware.md:27) - ⛏ Assertions that can't fail (middleware.rs:360, plus four more listed there)
- ⛏
TimingSnapshotlacks theD = ()default (request_timing.rs:45) - 🌱
PhaseSpanhas nofinish/cancel(request_timing.rs:260)
Not attached to a diff line:
-
⛏
configuration.mdnow contradicts the corrected middleware guide:docs/guide/configuration.md:49still says "Manifest-driven middleware are applied in order before routes". This PR corrected the same claim inmiddleware.md:61-62. Suggest reusing that sentence: "Routes are matched first. Middleware then run in registration order for a matched route; unmatched 404/405 requests bypass the middleware chain." -
🌱 The middleware fits only two of Trusted Server's four adapters: at
2791ceb, only Cloudflare and Spin register it.- Fastly creates its collector before the app is built, so it can time AppBuild (
main.rs:128-131). - Axum reimplements insert-if-absent plus the
/healthexclusion as a tower service outsideRouterService(timing.rs:78-99). That also covers 404/405 and gives it a final point to render headers.
This is the gap behind the earlier 🌱 about reaching the middleware-created handle after the router returns. An EdgeZero
Servicewrapper aroundRouterServicewould cover unmatched routes and give every adapter one place to finish timing. - Fastly creates its collector before the app is built, so it can time AppBuild (
CI Status
- fmt: PASS
- clippy (
--all-features -D warnings): PASS - tests (
--workspace --all-targets): PASS - doctests (
--workspace --doc): PASS (1 compile-fail test, 13 ignored) - feature check (
fastly cloudflare spin): PASS - Spin
wasm32-wasip2check: PASS - GitHub: 21/21 checks green
prk-Jr
left a comment
There was a problem hiding this comment.
😃 Reviewed all 11 changed files at 9c03cc5, including focused collector/API and middleware/adapter reviews. No actionable defects found.
The implementation preserves existing collectors, keeps phase and payload updates under one mutex, and follows the documented contention, cancellation, and explicit lifecycle contracts.
Local verification passed:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets --all-features -- -D warnings
- cargo test --workspace --all-targets
- cargo test -p edgezero-core: 484 unit tests, two integration tests, and the compile-fail doctest passed; 13 existing doctests ignored
- cargo check --workspace --all-targets --features "fastly cloudflare spin"
- cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spin
- Fastly WASM contract suite under Viceroy: all seven tests passed
- Docs lint and formatting
Initial sandbox socket-binding and certificate-store failures cleared on rerun outside the sandbox. All 22 GitHub checks were green at the reviewed commit. Cloudflare/Spin runtime suites and Trusted Server joint validation were not rerun as part of this review.
Keep core doctests in the workspace host suite alongside main's CLI test matrix.
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
Re-review at 878be472, which is ab444946 plus the merge of main. Both 🔧 items from the first review are fixed correctly. t0 now sits outside the mutex, so elapsed() is lock-free, and a cross-thread test covers it. TimingError and TimingSnapshot are #[non_exhaustive]. I checked every other claimed fix against the code and each one holds. The deferrals recorded in the spec's "Deferred follow-ups" are reasonable.
I'm requesting changes for the items below. The main ones are the Cloudflare/Spin gap in the guide's advice on where to measure, and the release gate on Trusted Server #1261.
Findings
Inline:
- 🤔 Cloudflare and Spin have no adapter hook for preinstalling a handle (docs/guide/middleware.md:288)
- ⛏ The doctest gate is missing from the local gate scripts and
CLAUDE.md(.github/workflows/test.yml:97) - ⛏ The spec and plan have stale consumer references, the wrong browser name and old evidence counts (spec:8)
- ⛏ Some process and authorization text remains (spec:478)
Not attached to a diff line:
- ⛏ The PR description is stale: it still says Trusted Server migration and joint verification are "complete against this exact candidate". The test plan's counts (484 unit tests; Cloudflare 8, Fastly 7, Spin 13) and the "22 checks … identical to
9c03cc59" note describe the original commit, and the public API has been renamed since then. The Changes table also leaves out.github/workflows/test.ymland theedgezero-macrosfixture. Suggest refreshing it and marking joint validation as pending, as the spec's "Remaining release checkpoints" already does. - 📝 The release gate isn't met yet: Trusted Server #1261 pins all six crates to
ab444946. Its Fastly adapter usesedgezero_adapter_fastly::lifecycle::{Sandbox, run_custom}, which comes from #379 and exists on this branch only after the merge. As a result its CI fails to compile (E0433 attrusted-server-adapter-fastly/src/main.rs:125). The spec's checkpoints put renewed joint validation before the EdgeZero merge, and that validation hasn't happened yet. #1261 needs to re-pin to878be472or later and pass.
CI Status
- fmt: PASS
- clippy (
--all-features -D warnings): PASS - tests (
--workspace --all-targets): PASS - core doctests (
-p edgezero-core --doc): PASS (1 compile-fail test, 13 ignored) - feature check (
fastly cloudflare spin): PASS - Spin
wasm32-wasip2check: PASS - GitHub: 28/28 checks green, including Cloudflare (10 tests, headless Firefox), Fastly/Viceroy (9) and Spin/Wasmtime (14)
| remain application-owned. Unmatched routes bypass middleware, and | ||
| `RouterService::oneshot` renders handler errors outside the chain. Use adapter or | ||
| outer-service hooks for terminal/error and streaming measurements as needed. | ||
| Those outer layers must retain a clone of the handle they preinstall; attachment |
There was a problem hiding this comment.
🤔 Cloudflare and Spin have no adapter hook for this: this paragraph sends final, error and streaming measurement to "adapter or outer-service hooks" that preinstall a handle and keep a clone. Fastly has that hook: run_app_with_request_extensions, serve_app_with_request_extensions and lifecycle::run_custom (see adapters/fastly.md:140). Axum can wrap the router in a tower service. Cloudflare and Spin don't, and they're the two adapters this middleware is for:
run_appanddispatch_apptake the platform request and convert it internally (edgezero-adapter-cloudflare/src/lib.rs:100,127,edgezero-adapter-spin/src/lib.rs:111,135).CloudflareService::dispatchand Spin'srequest::dispatchdo the same.dispatch_with_registriesispub(crate)in both crates (cloudflare/src/request.rs:298,spin/src/request.rs:183). Converting withinto_core_requestand dispatching by hand loses the manifest store bindings.
On those adapters, the outermost place available today is a middleware registered before RequestTimingMiddleware. I tested this one: it reuses a preinstalled handle without resetting its origin, and it marks headers ready on a 200 and on a handler error (400) before oneshot renders the error. 404/405 requests skip it.
struct TimingBoundary;
#[async_trait(?Send)]
impl Middleware for TimingBoundary {
async fn handle(&self, mut ctx: RequestContext, next: Next<'_>) -> Result<Response, EdgeError> {
let existing = ctx.request().extensions().get::<Handle>().cloned();
let timings = existing.unwrap_or_else(Handle::new);
ctx.request_mut().extensions_mut().insert(timings.clone());
let result = next.run(ctx).await;
let _ = timings.mark_headers_ready();
result
}
}Suggest naming the hook for each adapter here, and showing this pattern for Cloudflare and Spin with its limits: it doesn't cover unmatched routes or streamed bodies. A dispatch_app_with_request_extensions for Cloudflare and Spin, matching Fastly's, would close the gap. If it doesn't land in this PR, it belongs in the spec's deferred follow-ups.
|
|
||
| - name: Core doctests | ||
| if: matrix.suite == 'workspace' | ||
| run: cargo test -p edgezero-core --doc |
There was a problem hiding this comment.
⛏ This gate only exists in CI: scripts/run_tests.sh:36, .claude/commands/check-ci.md and the "CI Gates" list in CLAUDE.md all stop at cargo test --workspace --all-targets, which skips doctests. So a local "all gates pass" never runs the compile_fail test that keeps the snapshot payload from escaping. Suggest adding cargo test -p edgezero-core --doc to all three.
| Scope: `edgezero-core` and coordinated Trusted Server adoption, shipped together. | ||
| Plan: [Implementation plan](../plans/2026-09-28-request-timing.md). | ||
|
|
||
| The initial candidate is [EdgeZero #389](https://github.com/stackpop/edgezero/pull/389) |
There was a problem hiding this comment.
⛏ These records have gone stale again:
- Trusted Server PR: #1121 was closed on 2026-10-09 and superseded by #1261, which pins
ab444946. This paragraph and plan lines 19-25 still describe #1121 at2791cebas the consumer. - Browser: spec lines 111 and 389 and plan line 299 say the browser harness is Chromium. CI runs the Cloudflare contract suite in headless Firefox (job log: "Running headless tests in Firefox", 10 passed). Chromium is what ran locally, so "headless browser (Firefox in CI)" would be accurate.
- Evidence counts: the plan's "Review revision verification" table (lines 359-380) counts 8 Fastly and 9 Cloudflare contract tests, from before the merge of
main. CI now runs 9 and 10.
| 3. Approve the EdgeZero merge/tag through its normal release process, then replace | ||
| temporary TS pins with that tag and perform final tag-backed validation. | ||
|
|
||
| Publication, merge, releases, GitHub replies and thread resolution require their |
There was a problem hiding this comment.
⛏ Some process text remains: the earlier thread asked to drop the authorization narrative, and a few lines are still here:
- Here (lines 478-479): "Publication, merge, releases, GitHub replies and thread resolution require their own authorization; this record does not grant it."
- Line 94: "Explain this narrower scope in the review response…"
- Plan lines 389-391 and 407-409: "…subject to separate authorization" and "Reply to Aram with evidence… Resolve review threads only with separate authorization."
- Plan lines 361-365: a SHA256 of the inputs to an uncommitted tree, which no reader can reproduce.
These describe how the work was run, not the design, so they'll read as noise once this lands on main. Suggest deleting them.
…ression tests, synchronize local doctest gates, refresh verification records, and remove session-only narrative. Production behavior unchanged.
…t changing behavior.
Summary
Changes
edgezero-core/src/request_timing.rs,src/lib.rsRequestTimings<N, D>, consistent snapshots, compound updates, saturating accumulation, drop-time phase spans and explicit lifecycle marksedgezero-core/src/middleware.rsApplications retain phase enums, domain facts, header names/order, serialization and exposure policy. In particular, Trusted Server retains
Server-Timingrendering; no generic renderer is introduced. Callbacks run synchronously under one nonblocking mutex; panics propagate and poison recovery is best-effort, without rollback or payload validation. Attachment does not finalize requests, measure streamed-body completion, or cover unmatched routes.Closes
No standalone EdgeZero issue. Related consumer work: IABTechLab/trusted-server#1121.
Test plan
Publication-stage reruns against this tree:
cargo test -p edgezero-core— 484 unit tests, two integration tests and one compile-fail doctest passed; 13 existing doctests ignoredcargo test --workspace --all-targetscargo clippy --workspace --all-targets --all-features -- -D warningscargo fmt --all -- --checkcargo check --workspace --all-targets --features "fastly cloudflare spin"cargo check -p edgezero-adapter-spin --target wasm32-wasip2 --features spingit diff --checkEarlier implementation-stage validation, recorded in the committed plan (not rerun during publication):
wasm32-wasip1, Spinwasm32-wasip2, Cloudflarewasm32-unknown-unknownexamples/app-demogates2791ceb46908e6af04ccaae0c10b29d35336a9f4Independent final verification
9c03cc59300363ae5339fc970dded08ede563e98.2791ceb46908e6af04ccaae0c10b29d35336a9f4before requesting review.Coordinated delivery / merge gate
Trusted Server temporarily pins all six EdgeZero dependencies to
9c03cc59300363ae5339fc970dded08ede563e98for integration and joint PR verification, with no local path override and one resolved EdgeZero core identity. That temporary commit pin is not the final delivery pin.Trusted Server #1121 needs an actual approved EdgeZero release tag before merging to main. Replace all temporary pins with that release, regenerate and inspect its lockfile, and revalidate. Review readiness does not waive the release requirement. No merge or tag/release has been performed.
Checklist
{id}syntax where applicable; routing unchangededgezero_core, not directly fromhttp