Skip to content

Commit 46734a1

Browse files
radu-mateiclaude
andcommitted
feat: sync stateful components to Spin v4 (WASI RC 2026-03-15)
Bump the spin-stateful-http world and its vendored WASI deps from wasi:http/handler / wasi:clocks RC 2026-01-06 to 2026-03-15 to match Spin v4, sync the vendored lifecycle.wit with the host, and add a stateful-counter example (router + counter, addressed via spin.alt). Validated end-to-end on a Spin v4 host: the example component builds with a WASI-0.3-capable componentize-py (bytecodealliance/componentize-py#225) and exports spin:stateful-component/lifecycle@0.1.0 + wasi:http/handler@0.3.0-rc-2026-03-15; instantiate/suspend lifecycle, per-instance isolation, and re-instantiation all work. The p3 Handler/Lifecycle protocols in wit/exports remain hand-maintained stubs: componentize-py 0.17.2 can't generate p3 stream bindings, and the WASI-0.3 componentize-py can't run the 'bindings' regen against wit/deps because the SDK vendors multiple fermyon:spin versions. Building components works regardless. See CONTRIBUTING.md and examples/spin-stateful/README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 266c538 commit 46734a1

15 files changed

Lines changed: 449 additions & 70 deletions

File tree

CONTRIBUTING.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ mv bindings/spin_sdk/wit/* src/spin_sdk/wit/
3232
rm -r bindings
3333
```
3434

35+
> **Note on stateful / WASI Preview 3 components.** The `spin-stateful-http`
36+
> world exports `wasi:http/handler@0.3.0-rc-2026-03-15`, whose request/response
37+
> bodies are `stream<u8>`. `componentize-py` 0.17.2 cannot regenerate bindings
38+
> for p3 stream types (it panics with `not yet implemented: Stream(...)`), so the
39+
> `Handler` and `Lifecycle` protocols in `src/spin_sdk/wit/exports/__init__.py`
40+
> are maintained by hand rather than regenerated by the command above.
41+
>
42+
> Building a stateful *component* (as opposed to regenerating bindings) requires
43+
> a `componentize-py` with WASI 0.3 support
44+
> ([bytecodealliance/componentize-py#225](https://github.com/bytecodealliance/componentize-py/pull/225)),
45+
> built from that branch until it is released. With it, build with:
46+
>
47+
> ```bash
48+
> componentize-py --all-features componentize -m spin_sdk=spin-stateful-http app -o app.wasm
49+
> ```
50+
>
51+
> Note: that newer `componentize-py` rejects the `bindings -d src/spin_sdk/wit`
52+
> regeneration above because the SDK vendors multiple `fermyon:spin` package
53+
> versions (`@2.0.0`, `@3.0.0`, unversioned) under `wit/deps`; consolidating
54+
> those to a single version is needed to regenerate (rather than hand-maintain)
55+
> the p3 bindings. See `examples/spin-stateful`.
56+
3557
### Updating docs
3658
3759
Any time you regenerate the bindings or edit files by hand, you'll want to

examples/spin-stateful/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Stateful counter (Python)
2+
3+
A [stateful component](https://spinframework.dev): a long-lived, addressable
4+
Wasm instance. The runtime keeps one live instance per `(component, instance-id)`
5+
pair, calls the lifecycle hooks, and routes requests to it via the `spin.alt`
6+
addressing scheme. The count lives in memory across requests for the lifetime of
7+
the instance.
8+
9+
```python
10+
from spin_sdk.stateful import stateful_component, Request, Response
11+
12+
@stateful_component
13+
class Counter:
14+
def __init__(self, id: str): # called once on activation
15+
self.id = id
16+
self.count = 0
17+
18+
def suspend(self): # called before idle suspension (optional)
19+
print(f"suspending counter {self.id} at {self.count}")
20+
21+
def handle_request(self, request: Request) -> Response:
22+
self.count += 1
23+
return Response(200, {"content-type": "text/plain"},
24+
f"counter {self.id} = {self.count}\n".encode())
25+
```
26+
27+
Stateful components are not publicly routable; pair this with a router component
28+
that forwards `GET /<id>` to `https://spin.alt/component/counter/<id>/` (see the
29+
Rust SDK's `stateful-counter` example for a complete router + counter app).
30+
31+
## Building
32+
33+
> **Tooling requirement.** Stateful components export the WASI Preview 3 HTTP
34+
> handler (`wasi:http/handler@0.3.0-rc-2026-03-15`), whose bodies are
35+
> `stream<u8>`. This needs a `componentize-py` with WASI 0.3 support
36+
> ([bytecodealliance/componentize-py#225](https://github.com/bytecodealliance/componentize-py/pull/225)),
37+
> which is newer than the 0.17.2 pinned in `CONTRIBUTING.md`. Build it from that
38+
> branch until a release is available.
39+
40+
```bash
41+
componentize-py --all-features componentize \
42+
-m spin_sdk=spin-stateful-http \
43+
app -o app.wasm
44+
```
45+
46+
`-m spin_sdk=spin-stateful-http` selects the stateful world from the SDK's
47+
`componentize-py.toml` (which exposes several worlds), and `--all-features`
48+
enables the `@since`/`@unstable` WASI annotations. If the SDK isn't installed in
49+
the active environment, add `-p <path-to>/spin-python-sdk/src`.
50+
51+
## Running end-to-end
52+
53+
Pair this counter with a router (e.g. the Rust SDK's `stateful-counter` router)
54+
and run on a Spin v4 host:
55+
56+
```bash
57+
spin up --stateful-idle-timeout 30s
58+
curl localhost:3000/alice # counter alice = 1
59+
curl localhost:3000/alice # counter alice = 2
60+
curl localhost:3000/bob # counter bob = 1 (isolated per instance)
61+
```

examples/spin-stateful/app.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""A stateful counter component.
2+
3+
The runtime keeps one live instance per (component, instance-id) pair and
4+
routes requests to it via the spin.alt addressing scheme. The count lives in
5+
memory across requests for the lifetime of the instance.
6+
"""
7+
8+
from spin_sdk.stateful import stateful_component, Request, Response
9+
10+
11+
@stateful_component
12+
class Counter:
13+
def __init__(self, id: str):
14+
# Called once when the runtime first activates this instance.
15+
self.id = id
16+
self.count = 0
17+
18+
def suspend(self):
19+
# Called before the runtime suspends this instance (idle timeout).
20+
print(f"suspending counter {self.id} at {self.count}")
21+
22+
def handle_request(self, request: Request) -> Response:
23+
self.count += 1
24+
body = f"counter {self.id} = {self.count}\n".encode()
25+
return Response(200, {"content-type": "text/plain"}, body)

examples/spin-stateful/spin.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
spin_manifest_version = 2
2+
3+
[application]
4+
name = "spin-stateful"
5+
version = "0.1.0"
6+
authors = ["The Spin authors"]
7+
description = "A stateful counter component addressed via spin.alt."
8+
9+
# The counter has no trigger of its own; it is reached via the spin.alt
10+
# addressing scheme from another component (e.g. a router). See README.md.
11+
[component.counter]
12+
source = "app.wasm"
13+
stateful = true
14+
[component.counter.build]
15+
command = "componentize-py --all-features componentize -m spin_sdk=spin-stateful-http app -o app.wasm"
16+
watch = ["app.py"]

src/spin_sdk/wit/deps/clocks-0.3.0-rc-2026-01-06/clocks.wit

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package wasi:clocks@0.3.0-rc-2026-03-15;
2+
/// WASI Monotonic Clock is a clock API intended to let users measure elapsed
3+
/// time.
4+
///
5+
/// It is intended to be portable at least between Unix-family platforms and
6+
/// Windows.
7+
///
8+
/// A monotonic clock is a clock which has an unspecified initial value, and
9+
/// successive reads of the clock will produce non-decreasing values.
10+
@since(version = 0.3.0-rc-2026-03-15)
11+
interface monotonic-clock {
12+
use types.{duration};
13+
14+
/// A mark on a monotonic clock is a number of nanoseconds since an
15+
/// unspecified initial value, and can only be compared to instances from
16+
/// the same monotonic-clock.
17+
@since(version = 0.3.0-rc-2026-03-15)
18+
type mark = u64;
19+
20+
/// Read the current value of the clock.
21+
///
22+
/// The clock is monotonic, therefore calling this function repeatedly will
23+
/// produce a sequence of non-decreasing values.
24+
///
25+
/// For completeness, this function traps if it's not possible to represent
26+
/// the value of the clock in a `mark`. Consequently, implementations
27+
/// should ensure that the starting time is low enough to avoid the
28+
/// possibility of overflow in practice.
29+
@since(version = 0.3.0-rc-2026-03-15)
30+
now: func() -> mark;
31+
32+
/// Query the resolution of the clock. Returns the duration of time
33+
/// corresponding to a clock tick.
34+
@since(version = 0.3.0-rc-2026-03-15)
35+
get-resolution: func() -> duration;
36+
37+
/// Wait until the specified mark has occurred.
38+
@since(version = 0.3.0-rc-2026-03-15)
39+
wait-until: async func(
40+
when: mark,
41+
);
42+
43+
/// Wait for the specified duration to elapse.
44+
@since(version = 0.3.0-rc-2026-03-15)
45+
wait-for: async func(
46+
how-long: duration,
47+
);
48+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package wasi:clocks@0.3.0-rc-2026-03-15;
2+
/// WASI System Clock is a clock API intended to let users query the current
3+
/// time. The clock is not necessarily monotonic as it may be reset.
4+
///
5+
/// It is intended to be portable at least between Unix-family platforms and
6+
/// Windows.
7+
///
8+
/// External references may be reset, so this clock is not necessarily
9+
/// monotonic, making it unsuitable for measuring elapsed time.
10+
///
11+
/// It is intended for reporting the current date and time for humans.
12+
@since(version = 0.3.0-rc-2026-03-15)
13+
interface system-clock {
14+
use types.{duration};
15+
16+
/// An "instant", or "exact time", is a point in time without regard to any
17+
/// time zone: just the time since a particular external reference point,
18+
/// often called an "epoch".
19+
///
20+
/// Here, the epoch is 1970-01-01T00:00:00Z, also known as
21+
/// [POSIX's Seconds Since the Epoch], also known as [Unix Time].
22+
///
23+
/// Note that even if the seconds field is negative, incrementing
24+
/// nanoseconds always represents moving forwards in time.
25+
/// For example, `{ -1 seconds, 999999999 nanoseconds }` represents the
26+
/// instant one nanosecond before the epoch.
27+
/// For more on various different ways to represent time, see
28+
/// https://tc39.es/proposal-temporal/docs/timezone.html
29+
///
30+
/// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16
31+
/// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time
32+
@since(version = 0.3.0-rc-2026-03-15)
33+
record instant {
34+
seconds: s64,
35+
nanoseconds: u32,
36+
}
37+
38+
/// Read the current value of the clock.
39+
///
40+
/// This clock is not monotonic, therefore calling this function repeatedly
41+
/// will not necessarily produce a sequence of non-decreasing values.
42+
///
43+
/// The nanoseconds field of the output is always less than 1000000000.
44+
@since(version = 0.3.0-rc-2026-03-15)
45+
now: func() -> instant;
46+
47+
/// Query the resolution of the clock. Returns the smallest duration of time
48+
/// that the implementation permits distinguishing.
49+
@since(version = 0.3.0-rc-2026-03-15)
50+
get-resolution: func() -> duration;
51+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package wasi:clocks@0.3.0-rc-2026-03-15;
2+
3+
@unstable(feature = clocks-timezone)
4+
interface timezone {
5+
@unstable(feature = clocks-timezone)
6+
use system-clock.{instant};
7+
8+
/// Return the IANA identifier of the currently configured timezone. This
9+
/// should be an identifier from the IANA Time Zone Database.
10+
///
11+
/// For displaying to a user, the identifier should be converted into a
12+
/// localized name by means of an internationalization API.
13+
///
14+
/// If the implementation does not expose an actual timezone, or is unable
15+
/// to provide mappings from times to deltas between the configured timezone
16+
/// and UTC, or determining the current timezone fails, or the timezone does
17+
/// not have an IANA identifier, this returns nothing.
18+
@unstable(feature = clocks-timezone)
19+
iana-id: func() -> option<string>;
20+
21+
/// The number of nanoseconds difference between UTC time and the local
22+
/// time of the currently configured timezone, at the exact time of
23+
/// `instant`.
24+
///
25+
/// The magnitude of the returned value will always be less than
26+
/// 86,400,000,000,000 which is the number of nanoseconds in a day
27+
/// (24*60*60*1e9).
28+
///
29+
/// If the implementation does not expose an actual timezone, or is unable
30+
/// to provide mappings from times to deltas between the configured timezone
31+
/// and UTC, or determining the current timezone fails, this returns
32+
/// nothing.
33+
@unstable(feature = clocks-timezone)
34+
utc-offset: func(when: instant) -> option<s64>;
35+
36+
/// Returns a string that is suitable to assist humans in debugging whether
37+
/// any timezone is available, and if so, which. This may be the same string
38+
/// as `iana-id`, or a formatted representation of the UTC offset such as
39+
/// `-04:00`, or something else.
40+
///
41+
/// WARNING: The returned string should not be consumed mechanically! It may
42+
/// change across platforms, hosts, or other implementation details. Parsing
43+
/// this string is a major platform-compatibility hazard.
44+
@unstable(feature = clocks-timezone)
45+
to-debug-string: func() -> string;
46+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package wasi:clocks@0.3.0-rc-2026-03-15;
2+
/// This interface common types used throughout wasi:clocks.
3+
@since(version = 0.3.0-rc-2026-03-15)
4+
interface types {
5+
/// A duration of time, in nanoseconds.
6+
@since(version = 0.3.0-rc-2026-03-15)
7+
type duration = u64;
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package wasi:clocks@0.3.0-rc-2026-03-15;
2+
3+
@since(version = 0.3.0-rc-2026-03-15)
4+
world imports {
5+
@since(version = 0.3.0-rc-2026-03-15)
6+
import monotonic-clock;
7+
@since(version = 0.3.0-rc-2026-03-15)
8+
import system-clock;
9+
@unstable(feature = clocks-timezone)
10+
import timezone;
11+
}

0 commit comments

Comments
 (0)