Skip to content

Add opt-in reusable application lifecycles - #379

Merged
prk-Jr merged 14 commits into
mainfrom
feat/reusable-app-lifecycle
Oct 6, 2026
Merged

prk-Jr merged 14 commits into
mainfrom
feat/reusable-app-lifecycle

Conversation

@prk-Jr

@prk-Jr prk-Jr commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add opt-in retained application lifecycles: Fastly serve_app and custom serving helpers, plus caller-owned apps dispatched through dispatch_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:

  • CLI validation: config push and config diff apply adapter-specific checks only to the selected adapter, case-insensitively. Shared schema and secret-presence checks remain. Run config validate --strict to validate portability across declared, registered adapters.
  • Spin CLI diagnostics: secret-reference validation identifies affected fields and rules without printing reference values. This is an explicit exception to the original error-redaction non-goal.
  • Cloudflare response headers: preserve repeated application headers, including every Set-Cookie; the first application value replaces a body-generated default. This applies to default and retained dispatch.

Review corrections

  • Pin fixture build output, honor $CARGO, disable Wrangler's automatic second build, and execute Axum's Cargo-reported artifact. Reject changed fingerprints across repetitions.
  • Assert retained HTTP state and specific logger/binding failures; retain contextual failure records. Make accepted backend sockets blocking and terminate runtime process groups.
  • Limit timing summaries to successful matched probes and report quantiles separately for each repetition. Remove the unused --require-runtime flag; unsupported/unverified evidence still exits 2.
  • Compile, format, lint, and cache all provider fixtures in CI. Update public API contracts, adoption guidance, and implemented spec/plan status.
  • Merge current main, keeping Store Capabilities under Available Adapters and documenting custom Fastly streaming alongside standard buffering.

Validation

  • All five repository CI gates: formatting, workspace tests, all-feature Clippy, all-target adapter feature checks, and Spin WASM compilation.
  • Workspace tests: 1,440 passed, one existing ignored test.
  • Fixture host tests: 23 harness tests and 2 portable tests; all fixture target checks and Clippy gates passed.
  • Fastly WASM contract tests: 7 passed. All adapter WASM Clippy checks passed.
  • Documentation formatting, ESLint, and VitePress build passed.
  • Local Fastly and Cloudflare HTTP smokes passed with an external CARGO_TARGET_DIR; build records selected the fresh fixture artifacts. Custom Fastly variants passed store-binding reads.
  • Local Axum smoke passed with CARGO_BUILD_TARGET set; its artifact path came from Cargo output.
  • Local Spin sequential retention and same-instance required-binding recovery passed. Per-request overlap passed; retained overlap remained unverified because the host selected separate guests, so the Spin suite exited 2.

Local evidence does not establish deployed eviction, long-lived resource validity, memory bounds, or application performance. No deployment was performed. Public Sandbox counters and custom serving APIs are documented in the updated spec; application refresh, key rotation, and error policy remain caller-owned.

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.
@prk-Jr prk-Jr self-assigned this Sep 15, 2026
@prk-Jr
prk-Jr marked this pull request as draft September 15, 2026 12:46
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.
@prk-Jr
prk-Jr marked this pull request as ready for review September 19, 2026 05:23

@ChristianPavilonis ChristianPavilonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread crates/edgezero-cli/src/config.rs Outdated
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.
@prk-Jr

prk-Jr commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 1aaa19a5.

Extracted is_unselected_adapter(selected, name) using eq_ignore_ascii_case, applied at all three filters (run_adapter_shared_checks, run_adapter_typed_checks, strict_capability_completeness), so the --adapter filter folds case the same way Manifest::adapter_entry and adapter_registry::get_adapter do.

Regression test selected_adapter_filter_matches_manifest_key_case_insensitively covers both directions: [adapters.Spin] + --adapter spin and [adapters.spin] + --adapter SPIN. Verified it fails against the old exact-string compare and passes with the fix. All CI gates green locally.

@aram356 aram356 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Capabilities first. Also update main's cloudflare.md sentence that says the registry dispatch path isn't public API, and add a serve_custom exception 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 + oneshot inserts no KV/Config/Secrets registries, and the registry builders are private. Expose into_core_request_with_registries, or at minimum document the gap. (inline: fastly.md:363)

  • 🔧 Harness runs stale artifacts when CARGO_TARGET_DIR is set: reproduced, it exits 0 against old wasm. (inline: runners.rs:14)

  • 🔧 build_identity mixed-build guard is unreachable: the key includes the repetition. (inline: evidence.rs:294)

  • ❓ Scope: three changes here aren't part of the lifecycle work:

    1. Selected-adapter scoping of config push/config diff validation, which changes what a push validates.
    2. Spin secret-reference redaction. The spec lists "error redaction" as a non-goal.
    3. 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).

Non-blocking

  • ♻️ dispatch_app should take A: Hooks instead of loose StoresMetadata, and run_app should delegate to it: this applies to both Cloudflare and Spin. (inline: cloudflare/src/lib.rs:146)
  • 🤔 serve_app docs: any dispatch Err ends the retained sandbox, not just initialization errors. (inline: fastly/src/lib.rs:611)
  • 🤔 Public API commitment: the PR ships public lifecycle::Sandbox and 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: shared is 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_custom is missing #[must_use]; config.rs has a misplaced doc comment and a duplicate ensure_adapter_defined; --require-runtime does nothing; fastly = "=0.12.1" is pinned exactly; there's a dead /constructor-panic branch and an "Arm B" label; a panic in the harness should be an error; the setup_once example and its rust,ignore fence; with_timeout isn'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, so Backend::start() can grab the same port. Start the backend first.
    • Process::drop signals 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.
    • /health and 503 replies are counted as "probe" latency samples.
    • terminated_guest_not_reused passes trivially for custom-a.
    • The Viceroy version isn't checked against .tool-versions.
    • The toolchain comes from PATH instead 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_state reads 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 Steps on fastly, cloudflare, spin and axum.
    • In app-demo-core/src/lib.rs:26, also mention Cloudflare and Spin, which build per request by default.
  • ⛏ Fixture nits:
    • Cloudflare retained mode never calls init_logger().
    • fixture-spin relies on the adapter enabling spin-sdk features http and key-value; declare them.
    • MAX_INFLIGHT is a guest-lifetime high-water mark, not per pair.
    • EdgeError::bad_request("missing proxy") should be internal.
    • limit_lifetime.rs asserts before finish() prints the summary.

📌 Out of Scope

  • crates/edgezero-adapter-cloudflare/src/proxy.rs:101 copies upstream response headers with insert. Per the Fetch spec, Headers iteration yields each Set-Cookie separately, so a proxied upstream response keeps only its last cookie. This is the same class of bug fixed here for app responses, and append is 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

Comment thread docs/guide/adapters/overview.md
Comment thread docs/guide/adapters/fastly.md Outdated
Comment thread tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs Outdated
Comment thread tests/fixtures/reusable-app/crates/fixture-harness/src/evidence.rs Outdated
Comment thread crates/edgezero-adapter-cloudflare/src/lib.rs Outdated
Comment thread docs/guide/adapters/fastly.md
Comment thread docs/guide/adapters/fastly.md Outdated
Comment thread docs/guide/cli-reference.md Outdated
Comment thread docs/superpowers/specs/2026-09-15-reusable-app-lifecycle-design.md Outdated
Comment thread tests/fixtures/reusable-app/README.md Outdated
@prk-Jr
prk-Jr requested a review from aram356 October 2, 2026 04:40

@aram356 aram356 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread docs/guide/adapters/cloudflare.md Outdated
Comment thread docs/guide/cli-reference.md Outdated
Comment thread docs/guide/adapters/fastly.md Outdated
Comment thread docs/guide/adapters/fastly.md Outdated
Comment thread docs/guide/adapters/fastly.md
Comment thread tests/fixtures/reusable-app/crates/fixture-harness/src/main.rs Outdated
Comment thread tests/fixtures/reusable-app/crates/fixture-harness/src/runners.rs Outdated
Comment thread tests/fixtures/reusable-app/Cargo.toml
Comment thread .github/workflows/format.yml
Comment thread docs/superpowers/plans/2026-09-15-reusable-app-lifecycle.md Outdated
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.
prk-Jr added 2 commits October 6, 2026 17:54
- 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.
@prk-Jr
prk-Jr merged commit 683202c into main Oct 6, 2026
22 checks passed
@dhruv8sh dhruv8sh mentioned this pull request Oct 7, 2026
15 of 16 tasks
prk-Jr added a commit that referenced this pull request Oct 8, 2026
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.
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.

4 participants