Skip to content
Closed
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
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ build-all: all

# Clean build artifacts
clean:
rm -rf $(BUILD_DIR) $(STUBS_DIR)
rm -rf $(BUILD_DIR) $(STUBS_DIR) fastly_compute/wit

# Code Generation
codegen: $(STUBS_DIR)
uv run python3 tools/codegen.py

# Development tools
lint: | $(STUBS_DIR)
Expand Down
73 changes: 73 additions & 0 deletions fastly_compute/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from typing import Any


class FastlyError(Exception):
"""Base class for all Fastly Compute exceptions."""

def __init__(self, message: str, wit_error: Any | None = None):
super().__init__(message)
self.wit_error = wit_error


class ResourceError(FastlyError):
"""Resource open/access errors."""

pass


class ResourceOpenError(ResourceError):
"""Error opening a resource."""

pass


class ResourceNotFound(ResourceError):
"""Resource not found."""

pass


class ResourceLimitExceeded(ResourceError):
"""Quotas or limits exceeded."""

pass


class BackendError(FastlyError):
"""Backend communication errors."""

pass


class BadRequestError(FastlyError):
"""Bad Request."""

pass


class RateLimitExceeded(FastlyError):
"""Rate limit exceeded."""

pass


# KV Store Specific
class KVStoreError(FastlyError):
pass


class KVKeyFound(KVStoreError):
pass


class KVPreconditionFailed(KVStoreError):
pass


class KVPayloadTooLarge(KVStoreError):
pass


# ACL Specific
class ACLError(FastlyError):
pass
79 changes: 79 additions & 0 deletions planning/acl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ACL API Design

## Overview

Design for interacting with Access Control Lists (ACLs).

## WIT Interface Reference

```wit
interface acl {
use types.{error, open-error, ip-address};
use http-body.{body};

resource acl {
open: static func(name: string) -> result<acl, open-error>;
lookup: func(ip-addr: ip-address) -> result<option<body>, acl-error>;
}

enum acl-error { too-many-requests, generic-error }
}
```

Generated stubs: `stubs/wit_world/imports/acl.py`

## API Design

```python
from typing import Optional, Any
from ipaddress import IPv4Address, IPv6Address
import json

class ACL:
"""An Access Control List."""

@classmethod
def open(cls, name: str) -> 'ACL': pass

def lookup(self, ip: str | IPv4Address | IPv6Address) -> Optional[dict]:
"""Look up an IP address in the ACL.

Returns:
Dictionary of metadata if found, None if not found.
"""
pass

def __enter__(self): return self
def __exit__(self, exc_type, exc_val, exc_tb): pass
```

## Usage Examples

```python
from fastly_compute import ACL

# Open an ACL
blocklist = ACL.open("my-blocklist")

client_ip = "203.0.113.1"

# Check if IP is in the ACL
entry = blocklist.lookup(client_ip)
if entry:
# IP found in blocklist
print(f"Blocked: {entry.get('reason', 'no reason')}")
return Response(status=403, body=b"Access Denied")

# IP not found
return Response(status=200, body=b"OK")
```

## Deferred Features

- **ACL Management**: Creating or updating ACLs via this API is not supported (read-only).
- **Non-IP Lookups**: Currently only IP address lookup is supported.

## Implementation Notes

1. **Body Parsing**: The WIT interface returns a `body` handle on match. The SDK should read and parse this body (assumed JSON) automatically for convenience.
2. **Error Handling**: Map `acl-error` to appropriate Python exceptions.
84 changes: 84 additions & 0 deletions planning/async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Async Python on Fastly Compute

## Overview

Analysis of asynchronous Python support on Fastly Compute, leveraging the `async-io` host interface.

## Reference Architecture

The **Spin Python SDK** provides a strong reference implementation for running `asyncio` on WASI-based platforms that lack full OS-level socket support but provide pollable host resources.

- **Reference**: `spin_sdk.http.poll_loop`
- **Mechanism**: Custom `asyncio.AbstractEventLoop` backed by host-side polling (`wasi:io/poll` in Spin, `fastly:compute/async-io` in Fastly).

## Fastly Async Primitives

The `compute.wit` defines the `async-io` interface:

```wit
interface async-io {
resource pollable { ... }
select: func(handles: list<borrow<pollable>>) -> u32;
select-with-timeout: func(handles: list<borrow<pollable>>, timeout-ms: u32) -> option<u32>;
}
```

Pollable resources include:
- `pending-response` (from `http-req.send-async`)
- `body` (HTTP bodies for streaming)
- `pending-lookup`, `pending-insert` (KV Store)
- `pending-entry` (Cache)

## Proposed Architecture

### 1. FastlyEventLoop

A custom event loop that implements `asyncio.AbstractEventLoop`.

- **Registry**: Maintains a mapping of `pollable` handles to Python `Future`s (wakers).
- **Run Loop**:
1. Executed synchronous tasks (`_run_once`).
2. Collects all active `pollable` handles.
3. Calls `async_io.select(handles)`.
4. Wakes up the corresponding Future for the ready handle.
- **Restrictions**: Methods relying on OS sockets (`create_connection`, `add_reader` for file descriptors) will raise `NotImplementedError`.

### 2. Async Primitives

We must provide async-native wrappers for host calls:

```python
async def send_async(request):
pending = http_req.send_async(request)
await wait_for(pending) # Registers with loop and yields
return pending.wait()
```

## Candidate Features for Async

| Feature | Async Benefit | Status |
| :--- | :--- | :--- |
| **Backend Requests** | **High**. Allows concurrent fetches (fan-out). | `send-async` exists. |
| **KV Store** | **High**. Non-blocking lookups. | `lookup-async` exists. |
| **Cache** | **Medium**. | `transaction-lookup-async` exists. |
| **Body Streaming** | **High**. Non-blocking read/write of streams. | Bodies are pollable. |
| **WebSockets** | **High**. Essential for websocket handling. | Future consideration. |

## ASGI Support

With `FastlyEventLoop`, we can support **ASGI** applications (FastAPI, Starlette, Quart).

- **Adapter**: `AsgiHttpIncoming` (similar to `WsgiHttpIncoming`).
- **Lifecycle**: Manages the ASGI `scope`, `receive`, and `send` channels mapping to Fastly Request/Response/Body.

## Risks & Challenges

1. **Socket Incompatibility**: Standard Python async libraries (e.g., `asyncpg`, `redis`, `aiohttp`) rely on `selector` based loops and TCP sockets. They **will not work** out of the box. Users must use Fastly-native clients (KV Store, Backend fetch) instead.
2. **Performance**: The overhead of the Python event loop in Wasm needs measurement.
3. **Maintenance**: Custom event loops are complex to maintain and ensure correctness against stdlib changes.

## Roadmap

1. **Phase 1 (PoC)**: Implement `FastlyEventLoop` and a simple `async_sleep` (using `select_with_timeout`).
2. **Phase 2 (HTTP)**: Implement `AsyncClient` for backend requests.
3. **Phase 3 (ASGI)**: Prototype a simple ASGI adapter for FastAPI.
79 changes: 79 additions & 0 deletions planning/cache-purge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Cache Purge API Design

## Overview

Design for programmatically purging content from the Fastly cache.

## WIT Interface Reference

```wit
interface purge {
use types.{error};

record purge-options {
soft-purge: bool,
extra: option<borrow<extra-purge-options>>,
}

purge-surrogate-key: func(
surrogate-keys: string,
purge-options: purge-options,
) -> result<_, error>;

purge-surrogate-key-verbose: func(
surrogate-keys: string,
purge-options: purge-options,
max-len: u64,
) -> result<string, error>;
}
```

Generated stubs: `stubs/wit_world/imports/purge.py`

## API Design

```python
from typing import Optional

def purge_surrogate_key(surrogate_key: str, soft: bool = False) -> None:
"""Purge content associated with a surrogate key.

Args:
surrogate_key: Key to purge (space-separated for multiple)
soft: If True, mark as stale rather than removing (Soft Purge)
"""
pass

def purge_surrogate_key_verbose(surrogate_key: str, soft: bool = False) -> str:
"""Purge content and return a purge ID.

Returns:
JSON string containing purge ID (e.g. {"1234-1234..."})
"""
pass
```

## Usage Examples

```python
from fastly_compute import purge_surrogate_key

# Hard purge
purge_surrogate_key("product:123")

# Soft purge (mark as stale)
purge_surrogate_key("catalog", soft=True)

# Multiple keys
purge_surrogate_key("product:123 category:electronics")
```

## Deferred Features

- **URL Purge**: Not supported by this interface (Surrogate Key purge is best practice).
- **Global Purge**: `purge_all` is not exposed here.

## Implementation Notes

1. **Surrogate Keys**: Fastly supports space-separated keys in a single string.
2. **Error Handling**: Raises exception on failure.
Loading
Loading