Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 3 additions & 5 deletions examples/flask-app.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand Down
2 changes: 1 addition & 1 deletion examples/game-of-life.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,4 @@ def root():


if running_under_compute:
HttpIncoming = WsgiHttpIncoming(app)
HttpIncoming = WsgiHttpIncoming(app, reuse_sessions_for_ms=300)
76 changes: 74 additions & 2 deletions fastly_compute/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
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.

We should rename this to reuse_sandboxes_for_ms, to align with Compute eg. https://github.com/fastly/ExecuteD/pull/5981.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ha, I got the timing exactly wrong! Will do.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

#21

):
"""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,
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not critical and possibly better left outside the SDK (or configurable), but I could see it potentially making sense to use intra-request periods as being good times to force gc with an otherwise relaxed gc policy to avoid it being hit while processing a request in most cases.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That's a neat idea. We could make it so there's a pluggable callback that we call inter-request, and you can take whatever breaths you need therein. A GC-nudging one could be the default or a publicly available one.