Repository navigation
Add opt-in reusable application lifecycles - #379
Conversation
Add retained Fastly serving helpers and explicit prebuilt-app dispatch for Cloudflare and Spin while preserving default lifecycles. Preserve repeated Cloudflare response headers. Add Rust lifecycle fixtures, isolation and cancellation coverage, local measurement tooling, CI integration, and adapter documentation with explicit provider validation limits.
Verify lazy initialization, handled failure, recovery and retained reuse in one Fastly guest. Carry request-specific finalization metadata through router dispatch and document the supported custom lifecycle boundary. Scope push and diff adapter validation to the selected destination while preserving standalone portability checks. Redact raw and normalized secret references from Spin diagnostics. Validated with workspace and fixture tests, Clippy, formatting, adapter feature checks, Spin WASM compilation, documentation checks and the local Fastly smoke suite.
Own successful-only lazy state retention, initialization attempts, callback counts, and independent setup in Sandbox<T>. Add reusable and single-request serving wrappers that preserve the SDK sending boundary and application-controlled streaming. Migrate custom lifecycle fixtures to the public helpers and document ownership across adapters. Verified workspace tests, fixture tests, Clippy, adapter compilation checks, formatting, and local Fastly streaming and recovery smoke tests.
Extract the production header-copy logic for host regression tests covering duplicate cookies, default replacement, invalid values, and sink errors. Exercise prebuilt dispatch with fresh request bodies in the Cloudflare browser contract suite. Document store metadata pairing, retained callback captures, and first-snapshot logging limitations without changing existing policies.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
The retained lifecycle and request-isolation paths are well covered, and the exact revision passes the relevant repository and runtime checks. I found one case-sensitive selected-adapter validation edge case, noted inline.
Adapter lookup is case-insensitive everywhere else (Manifest::adapter_entry, adapter_registry::get_adapter), but the --adapter selection filters in run_adapter_shared_checks, run_adapter_typed_checks, and strict_capability_completeness compared raw strings. So [adapters.Spin] with --adapter spin (or the reverse) passed ensure_adapter_defined but skipped the selected entry's manifest, merged-store, typed-secret, and store-capability checks, letting config push write invalid configuration that fails at runtime. Extract is_unselected_adapter and compare with eq_ignore_ascii_case at all three filters. Add a regression test covering both casing directions.
|
Fixed in Extracted Regression test |
aram356
left a comment
There was a problem hiding this comment.
PR Review
Summary
This PR adds opt-in retained app lifecycles for Fastly (serve_app, lifecycle::serve_custom), Cloudflare and Spin (dispatch_app), plus a fixture harness and docs. Four things need fixing before merge:
- The branch conflicts with main (#373).
- The custom streaming path gets no store registries, and the docs don't say so.
- The fixture harness can report a pass against stale artifacts.
- The guard meant to catch that can never fire.
I also have one scope question.
Findings
Blocking
-
🔧 Merge conflict in
docs/guide/adapters/overview.md: resolve with main's### Store Capabilitiesfirst. Also update main's cloudflare.md sentence that says the registry dispatch path isn't public API, and add aserve_customexception to the "Fastly always buffers" text in streaming.md and fastly.md. (inline: overview.md:128) -
🔧 Custom streaming path has no store registries:
into_core_request+oneshotinserts no KV/Config/Secrets registries, and the registry builders are private. Exposeinto_core_request_with_registries, or at minimum document the gap. (inline: fastly.md:363) -
🔧 Harness runs stale artifacts when
CARGO_TARGET_DIRis set: reproduced, it exits 0 against old wasm. (inline: runners.rs:14) -
🔧
build_identitymixed-build guard is unreachable: the key includes the repetition. (inline: evidence.rs:294) -
❓ Scope: three changes here aren't part of the lifecycle work:
- Selected-adapter scoping of
config push/config diffvalidation, which changes what a push validates. - Spin secret-reference redaction. The spec lists "error redaction" as a non-goal.
- The Cloudflare duplicate-header fix, which changes every Cloudflare response.
The PR description mentions only the header fix. Could the CLI and Spin changes go in their own PR, or at least be called out in the description and spec so they show up in the changelog? The code itself looks correct (inline nits on config.rs).
- Selected-adapter scoping of
Non-blocking
- ♻️
dispatch_appshould takeA: Hooksinstead of looseStoresMetadata, andrun_appshould delegate to it: this applies to both Cloudflare and Spin. (inline: cloudflare/src/lib.rs:146) - 🤔
serve_appdocs: any dispatchErrends the retained sandbox, not just initialization errors. (inline: fastly/src/lib.rs:611) - 🤔 Public API commitment: the PR ships public
lifecycle::Sandboxand calls the custom path a "supported interface", but the spec says the helper would be private. (inline: fastly.md:379) - 🤔 macOS non-blocking backend sockets in the harness (inline: net.rs:101)
- ♻️ Fixture evidence gaps:
sharedis never checked, the negative logger check accepts any 500, and binding errors are discarded. (inline: fixture-core:131, evidence.rs:55, fixture-cloudflare:28) - ♻️ CI doesn't compile fixture-cloudflare, fixture-spin or fixture-axum, and doesn't lint or cache the fixture workspace. (inline: test.yml:68)
- ⛏
serve_customis missing#[must_use]; config.rs has a misplaced doc comment and a duplicateensure_adapter_defined;--require-runtimedoes nothing;fastly = "=0.12.1"is pinned exactly; there's a dead/constructor-panicbranch and an "Arm B" label; a panic in the harness should be an error; thesetup_onceexample and itsrust,ignorefence;with_timeoutisn't explained; cli-reference placement; the spec and plans are stale; the README reads like a lab notebook. (inline) - 🌱 Harness robustness (follow-ups):
net::port()binds and drops a port before Viceroy starts, soBackend::start()can grab the same port. Start the backend first.Process::dropsignals only the direct child. Use a process group so wrangler → workerd grandchildren die on the SIGKILL fallback.- The idle-reuse check has only a 300 ms margin over the guest's 500 ms timeout.
- Repetitions are pooled into one quantile set, so the README's latency differences have no variance estimate.
- Failure records don't name the variant or repetition.
/healthand 503 replies are counted as "probe" latency samples.terminated_guest_not_reusedpasses trivially forcustom-a.- The Viceroy version isn't checked against
.tool-versions. - The toolchain comes from
PATHinstead of$CARGO.
- 🌱 Docs adoption:
- Add a short "when to use / when not to" at the top of the overview section: use it when construction is measurably expensive, skip it when
configure/app_statereads values with no refresh policy. - Mention that the pinned Viceroy 0.17.0 admits at most 6 requests per guest.
- Move the new sections above
## Next Stepson fastly, cloudflare, spin and axum. - In app-demo-core/src/lib.rs:26, also mention Cloudflare and Spin, which build per request by default.
- Add a short "when to use / when not to" at the top of the overview section: use it when construction is measurably expensive, skip it when
- ⛏ Fixture nits:
- Cloudflare retained mode never calls
init_logger(). - fixture-spin relies on the adapter enabling
spin-sdkfeatureshttpandkey-value; declare them. MAX_INFLIGHTis a guest-lifetime high-water mark, not per pair.EdgeError::bad_request("missing proxy")should beinternal.limit_lifetime.rsasserts beforefinish()prints the summary.
- Cloudflare retained mode never calls
📌 Out of Scope
crates/edgezero-adapter-cloudflare/src/proxy.rs:101copies upstream response headers withinsert. Per the Fetch spec,Headersiteration yields eachSet-Cookieseparately, so a proxied upstream response keeps only its last cookie. This is the same class of bug fixed here for app responses, andappendis correct either way. Worth a follow-up issue.
CI Status
- fmt: PASS
- clippy (
--all-features -D warnings): PASS - tests (
--workspace --all-targets): PASS - feature check (
fastly cloudflare spin): PASS - Spin wasm32-wasip2 check: PASS
- Fixture host tests (
-p fixture-harness -p fixture-core, locked): PASS (17 + 2) - Fixture wasm checks (fastly/wasip1, spin/wasip2, cloudflare/wasm32-unknown-unknown, axum): PASS
- Fastly lifecycle smoke (local, Viceroy 0.21.0): PASS, exit 0. Cheap probe: about 0.3 ms p50 reused vs 1.3–2.7 ms cold.
- GitHub checks: all green
- Mergeable: no, conflict in
docs/guide/adapters/overview.md
aram356
left a comment
There was a problem hiding this comment.
PR Review (follow-up at 35a7283)
Summary
All four blocking findings from the previous review are fixed, and the scope question is answered. I verified each fix against the code and by running it, not only from the replies. Approving.
The inline comments are non-blocking and resolvable; fix them here or in a follow-up. The two most worth doing soon are harness regressions introduced by the fixes: Ctrl-C now orphans runtime processes, and a Viceroy version mismatch skips the whole suite.
Previous findings
| Finding | Status |
|---|---|
| Merge conflict and post-merge doc contradictions | Fixed. One stale sentence remains in streaming.md:121 (inline) |
| Custom streaming path had no store registries | Fixed: request::into_core_request_with_registries, used by fixtures and checked by the harness for all custom variants |
| Harness ran stale artifacts | Fixed; reproduced with external CARGO_TARGET_DIR and CARGO_BUILD_TARGET_DIR |
build_identity guard unreachable |
Fixed, with a regression test |
| Scope (CLI scoping, Spin redaction, Cloudflare headers) | Answered in the PR description and spec |
dispatch_app pairing, run_app duplication |
Fixed: dispatch_app::<A>, and run_app delegates to it |
serve_app error semantics |
Fixed in rustdoc; guide wording still vague (inline) |
Retained shared state, negative-logger body, binding error preserved |
Fixed |
| CI fixture build, lint and cache coverage | Fixed; duplicate fastly fixture clippy (inline) |
--require-runtime no-op |
Removed; one leftover in the plan (inline) |
macOS sockets, interrupted-stream panic, must_use, config.rs nits, fastly pin, dead branch, guide/spec items |
Fixed |
CI Status
- fmt: PASS
- clippy (
--all-features -D warnings): PASS - tests (
--workspace --all-targets): PASS (1,440 passed, 1 ignored) - feature check (
fastly cloudflare spin): PASS - Spin wasm32-wasip2 check: PASS
- Fastly and Cloudflare adapter wasm clippy: PASS
- Fixture tests (harness 23, core 2), fmt, native and wasm clippy for all fixture crates: PASS
- Fastly lifecycle smoke (Viceroy 0.17.0, including with an external target dir): PASS. Axum smoke: PASS. Spin smoke (Spin 4.1.0): exit 2, because the host never reused an instance; nothing failed. Cloudflare smoke not run locally.
- GitHub checks: all green
- Mergeable: yes
Docs: - Fastly guide: add a complete serve_custom skeleton, state precise error semantics, use app.router(), avoid echoing setup errors to clients, clarify request vs callback wording, billing and Viceroy caveats. - Correct streaming, cloudflare, overview, spin, axum, handlers, configuration and cli-reference wording; document what --adapter skips. - Trim the fixture README to commands, assertions and limits; move notes into the spec. Drop the removed --require-runtime flag from the plan. - CI: skip the duplicate fastly fixture clippy on the "fastly cli" row. Fixtures: - Custom Fastly dispatch installs its logger via Sandbox::setup_once and joins the logging checks; gate post-send work on the streaming route. - Share MissingBindingApp and the loopback check in fixture-core; map missing bindings to EdgeError::internal without unwraps. - Remove dead max_inflight and configures outputs; route /other through run_app in per-request mode; Spin logger parity and ~6.0 pin. Harness: - Kill live runtime process groups on Ctrl-C (ctrlc crate). - Record Viceroy version mismatch as unverified and keep running. - Single contextual fail record per failure, including startup failures, with a phase field; fail record for mixed build fingerprints. - Serve and fingerprint artifacts copied into the run directory. - Extract settle/cohort_stats helpers; resolve rustc next to $CARGO.
- Custom Fastly fixture: log logger-setup failures to stderr and send a bare 503 instead of echoing the error, matching the guide. - Harness: only skip the generic fail record when a runner already recorded the same error, so an earlier non-fatal failure cannot hide it. - Docs: ServeSummary may be inspected or converted; list EdgeError render failure among terminal errors (guide and serve_app rustdoc); push keeps strict handler-path checks under --adapter; app.router() in rustdoc; qualify the heap-snapshot and setup-once README claims.
- Drop the stale spin.toml copy from the run directory; the served manifest is the rewritten spin-<mode>.toml pointing at staged wasm. - README: note that runtimes serve and fingerprint staged artifact copies. - Plan: exit 2 covers unverified or unsupported evidence. - CLI reference: fix push --adapter sentence grammar.
Combine the reusable application lifecycles (#379) with explicit request ingress fidelity. Fastly registry dispatch now converts through into_core_request_with_registries_and_ingress, so the request-only entry point from main also inserts the ingress snapshot after the extension callback, and dispatch_with_registries keeps capturing ingress before conversion. Keep both new CI steps and both new overview guide sections.
Summary
Add opt-in retained application lifecycles: Fastly
serve_appand custom serving helpers, plus caller-owned apps dispatched throughdispatch_app::<A>on Cloudflare and Spin. Default entry points retain their lifecycle defaults; request conversion, bindings, and extensions remain fresh.Fastly custom streaming now uses the same registry-aware request conversion as standard dispatch, while leaving response extensions and streams available to the caller. Provider fixtures verify initialization, request isolation, store access, failure recovery, headers, and actual guest reuse.
Included compatibility and CLI changes
These changes also affect existing users and are explicitly recorded in the design spec:
config pushandconfig diffapply adapter-specific checks only to the selected adapter, case-insensitively. Shared schema and secret-presence checks remain. Runconfig validate --strictto validate portability across declared, registered adapters.Set-Cookie; the first application value replaces a body-generated default. This applies to default and retained dispatch.Review corrections
$CARGO, disable Wrangler's automatic second build, and execute Axum's Cargo-reported artifact. Reject changed fingerprints across repetitions.--require-runtimeflag; unsupported/unverified evidence still exits 2.main, keeping Store Capabilities under Available Adapters and documenting custom Fastly streaming alongside standard buffering.Validation
CARGO_TARGET_DIR; build records selected the fresh fixture artifacts. Custom Fastly variants passed store-binding reads.CARGO_BUILD_TARGETset; its artifact path came from Cargo output.Local evidence does not establish deployed eviction, long-lived resource validity, memory bounds, or application performance. No deployment was performed. Public
Sandboxcounters and custom serving APIs are documented in the updated spec; application refresh, key rotation, and error policy remain caller-owned.