From db6c9b7c93c9c881e232232cda7128b2c27bf317 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Thu, 30 Oct 2025 11:56:18 -0400 Subject: [PATCH 1/2] Add Fastly session-reuse to WSGI HTTP handler, and demo it in game-of-life. Implementation is ugly as sin due to the early state of Python WIT bindings, but the user-facing API should be fine. --- examples/README.md | 9 +++-- examples/game-of-life.py | 2 +- fastly_compute/wsgi.py | 76 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/examples/README.md b/examples/README.md index 07601c6..5ceaeab 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,16 +6,19 @@ This directory contains example applications demonstrating different approaches ### `wit-bottle.py` - **Framework**: Bottle (lightweight WSGI framework) -- **Features**: Basic routing, JSON responses, WIT API integration +- **Shows**: Basic routing, JSON responses, WIT API integration - **Use Case**: Simple services, proof-of-concept applications ### `flask-app.py` - **Framework**: Flask (popular Python web framework) -- **Features**: Flask routing, request handling, error handling +- **Shows**: Flask routing, request handling, error handling - **Use Case**: More complex applications, familiar Flask patterns ### `game-of-life.py` -A server-side implementation of Conway’s Game of Life, with a server round trip per frame. This demonstrates raw requests-per-second performance. +A server-side implementation of Conway’s Game of Life, with a server round trip per frame. + +- **Shows**: Raw requests-per-second performance; Fastly's session-reuse +feature, which saves spin-up time in busy services ## Building and Running Examples diff --git a/examples/game-of-life.py b/examples/game-of-life.py index a7ce80d..83d7c10 100644 --- a/examples/game-of-life.py +++ b/examples/game-of-life.py @@ -263,4 +263,4 @@ def root(): if running_under_compute: - HttpIncoming = WsgiHttpIncoming(app) + HttpIncoming = WsgiHttpIncoming(app, reuse_sessions_for_ms=300) diff --git a/fastly_compute/wsgi.py b/fastly_compute/wsgi.py index 97d44be..1273bfa 100644 --- a/fastly_compute/wsgi.py +++ b/fastly_compute/wsgi.py @@ -16,7 +16,14 @@ from wit_world.exports import HttpIncoming as WitHttpIncoming from wit_world.imports import http_body, http_resp +from wit_world.imports.http_downstream import ( + NextRequestOptions, + await_request, + next_request, +) +from wit_world.imports.http_req import send from wit_world.imports.http_resp import send_downstream +from wit_world.imports.types import Err, Error_OptionalNone def serve_wsgi_request( @@ -128,18 +135,83 @@ def hello(): ``` """ - def __init__(self, wsgi_app: Callable, handle_errors: bool = False): + def __init__( + self, + wsgi_app: Callable, + handle_errors: bool = False, + reuse_sessions_for_ms: int = 0, + ): + """Construct. + + :arg wsgi_app: The WSGI app to which to delegate requests + :arg handle_errors: If True, log any raised exception and return a + 500-status response. + :arg reuse_sessions_for_ms: If non-0, keep the service instance alive + for this many milliseconds to potentially serve additional requests. + """ self.wsgi_app = wsgi_app self.handle_errors = handle_errors + self.reuse_sessions_for_ms = reuse_sessions_for_ms def __call__(self): return self def handle(self, request: Any, body: Any) -> None: - """Handle incoming HTTP request by serving it through the WSGI app.""" + """Handle incoming HTTP requests by serving them through the WSGI app.""" serve_wsgi_request( request, body, self.wsgi_app, handle_errors=self.handle_errors, ) + + if not self.reuse_sessions_for_ms: + return + + try: + # Drop (in the WIT sense) the `request` resource to get ready for + # another request. Otherwise, we crash. + # + # Here we abuse an arbitrary request-consuming function to trigger + # the drop. Glue code interposed by wasmtime's linker ensures that + # drop happens, but send() otherwise fails before doing anything. + send(request, body, "no such backend") + + # TODO: Generate a proper drop_whatever() function for each + # "whatever" resource. + # + # Alternately, it might suffice for the runtime to drop() things + # that get GC'd (i.e. `del` or otherwise) by Python. If we put an + # idiomatic .close() or similar on, for example, a potentially large + # request body, we could implement it in terms of `del`. + except Err: + pass + else: + raise RuntimeError( + "Our use of send() to consume the previous request unexpectedly actually performed a send." + ) + + options = NextRequestOptions(timeout_ms=self.reuse_sessions_for_ms, extra=None) + while True: + pending_request = next_request(options) + try: + result = await_request(pending_request) + except Err as exc: + # TODO: Improve error design so we can catch only the exceptions + # we're really interested in, per Python's idiom. Rather than + # carting around a Result type that's Union[Ok[T], Err[E]], we + # should probably return T xor raise E. + if isinstance(exc.value, Error_OptionalNone): + # There were no more requests within the timeout. + break + else: + # Something went wrong. + raise + else: + request, body = result + serve_wsgi_request( + request, + body, + self.wsgi_app, + handle_errors=self.handle_errors, + ) From e768a2d4d0b3e0c86820e2666b5ff968d3271c46 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 4 Nov 2025 14:33:20 -0500 Subject: [PATCH 2/2] Remove some out-of-date comments. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sys` is no longer needed to pull in shims. `noqa`s don’t seem to be needed either. --- examples/flask-app.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/flask-app.py b/examples/flask-app.py index 1c62e3b..c84bab6 100644 --- a/examples/flask-app.py +++ b/examples/flask-app.py @@ -1,11 +1,9 @@ -# Import and install WASI shims before importing Flask import sys -# Now we can import Flask and Fastly Compute modules -from flask import Flask, request # noqa: E402 -from wit_world.imports import compute_runtime # noqa: E402 +from flask import Flask, request +from wit_world.imports import compute_runtime -from fastly_compute.wsgi import WsgiHttpIncoming # noqa: E402 +from fastly_compute.wsgi import WsgiHttpIncoming # Create Flask app app = Flask(__name__)