You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Replace turboAPI's `zig/src/server.zig` (109 KB of HTTP/1.1 plumbing) with a thin Python FFI dispatcher that drives nanoapi's runtime layer. After this lands, turboAPI gets io_uring on Linux, kqueue on macOS, SO_REUSEPORT multicore, and the pre-rendered static-response cache — none of which it has today — without forking or duplicating the HTTP wire layer.
Conceptually: nanoapi becomes turboAPI's `server.zig`. turboAPI keeps its Python FFI glue (GIL/free-threading shims, async pool, dhi pre-GIL validation, the 5-tuple streaming contract from #164) and discards the duplicated socket/parse/dispatch code.
turboAPI#165 — pg.zig Zig 0.16 compat (blocks adding nanoapi to main build)
nanoapi#12 merged + tagged `v0.X.Y` — consumers pin to tag SHA in build.zig.zon, never main
Empirical proof of shape: `feat/nanoapi-runtime-spike` — a 68-line Zig binary in this repo's `spike/` constructs a `nanoapi.server.Dispatcher` with no `App`, calls `serveGeneric`, serves real HTTP. The Python port replaces `SpikeContext` with an interpreter pointer and the dispatch body with the existing handler-tuple machinery.
Implementation plan
Step 1 — wire nanoapi as a build dep
After #165: add nanoapi to `zig/build.zig.zon` (pinned to tag SHA), import as `nanoapi` module in `zig/build.zig`, and add `lib.root_module.addImport("nanoapi", nanoapi_mod)`.
fn pyHandle(ctx: *anyopaque, req: *nanoapi.Request) anyerror!nanoapi.Response {
const self: *PyDispatcher = @ptrCast(@aligncast(ctx));
// 1. Acquire GIL (or check free-threading state)
// 2. Build Python request representation from req.method/path/headers/body
// 3. Call into turboAPI's existing Python dispatch (request_handler.py)
// 4. Receive 3-tuple (status, content_type, body) or
// 5-tuple (status, content_type, b"", iterator, headers) for streaming
// 5. Marshal back to nanoapi.Response (or to ResponseWriter for streaming)
// 6. Release GIL
}
```
Step 3 — replace server.zig accept loop
Currently `zig/src/main.zig` boots its own listener and dispatch. New shape:
```zig
const py_disp = try PyDispatcher.init(allocator, ...);
defer py_disp.deinit();
try nanoapi.server.serveGeneric(py_disp.dispatcher(), allocator, .{
.host = host,
.port = port,
.runtime = .auto, // io_uring on Linux, kqueue on macOS
.worker_threads = 0, // one worker per CPU
});
```
`zig/src/server.zig` (the 109KB file) gets deleted. Maybe keep multipart.zig if nanoapi's multipart isn't equivalent yet; verify with parity tests.
Step 4 — preserve the 5-tuple streaming path from #164
The existing streaming dispatch in `request_handler.py` returns either a 3-tuple (sync) or 5-tuple `(status, content_type, b"", iterator, headers)` (streaming). `PyDispatcher.handle` needs to detect the 5-tuple and use nanoapi's streaming primitives (`StreamingResponse` / `SseWriter` / `LLMStreamResponse`) instead of building a plain `Response`. May need a small `ResponseWriter` extension on the nanoapi side if the existing streaming-response API doesn't expose chunked-write directly enough — in which case file as a follow-up nanoapi issue.
The `py_atomic_shim.c` and `py_gil_shim.c` C files stay. dhi pre-GIL JSON validation runs before GIL acquisition in `PyDispatcher.handle`. The exact spot is the same as today's `zig/src/main.zig`.
Step 6 — turn parity green
Run turbohttp-parity against the ported turboAPI. Every test that was green on nanoapi should now be green on turboAPI too. CI gate the merge on PARITY.md being green.
Risks
Free-threading invariants. nanoapi's runtime is single-GIL-aware (it's pure Zig, no Python concept). The Python dispatch happens entirely in user code (`pyHandle`), so this should Just Work, but worth fuzzing.
dhi version skew. turboAPI pins dhi at `Fz3bn1CFAwD…`, nanoapi at `Fz3bn4aKAwD…`. Zig's package manager can resolve both into separate cache slots when imported via different module trees, but if any shared struct (e.g. a `dhi.Validator`) crosses the boundary, types won't unify. Plan: converge to a single dhi pin before this PR merges.
Benchmark regression. turboAPI's current numbers are 140k req/s. nanoapi's macOS numbers are ~149k. Linux io_uring takes nanoapi to 983k single-route. Expect turboAPI's macOS numbers to stay flat or improve slightly; Linux numbers to jump dramatically. If macOS regresses, root-cause before merging.
PR feat: real end-to-end SSE / StreamingResponse over Zig HTTP server (closes #163) #164 (SSE) interaction. That PR's `sendStreamingResponse` in turboAPI's server.zig gets deleted as part of step 3. The streaming logic moves into `PyDispatcher.handle` calling nanoapi's streaming primitives. Tests in `tests/test_sse_e2e.py` must stay green.
Goal
Replace turboAPI's `zig/src/server.zig` (109 KB of HTTP/1.1 plumbing) with a thin Python FFI dispatcher that drives nanoapi's runtime layer. After this lands, turboAPI gets io_uring on Linux, kqueue on macOS, SO_REUSEPORT multicore, and the pre-rendered static-response cache — none of which it has today — without forking or duplicating the HTTP wire layer.
Conceptually: nanoapi becomes turboAPI's `server.zig`. turboAPI keeps its Python FFI glue (GIL/free-threading shims, async pool, dhi pre-GIL validation, the 5-tuple streaming contract from #164) and discards the duplicated socket/parse/dispatch code.
Dependencies
Empirical proof of shape: `feat/nanoapi-runtime-spike` — a 68-line Zig binary in this repo's `spike/` constructs a `nanoapi.server.Dispatcher` with no `App`, calls `serveGeneric`, serves real HTTP. The Python port replaces `SpikeContext` with an interpreter pointer and the dispatch body with the existing handler-tuple machinery.
Implementation plan
Step 1 — wire nanoapi as a build dep
After #165: add nanoapi to `zig/build.zig.zon` (pinned to tag SHA), import as `nanoapi` module in `zig/build.zig`, and add `lib.root_module.addImport("nanoapi", nanoapi_mod)`.
Step 2 — build a `PyDispatcher` in Zig
New file `zig/src/py_dispatcher.zig`. Holds:
```zig
pub const PyDispatcher = struct {
interp: *anyopaque, // PyInterpreterState pointer (or whatever turboAPI uses)
routes: *PyObject, // turboAPI's Router instance from Python
// ... whatever else the existing zig→python dispatch needs
};
fn pyHandle(ctx: *anyopaque, req: *nanoapi.Request) anyerror!nanoapi.Response {
const self: *PyDispatcher = @ptrCast(@aligncast(ctx));
// 1. Acquire GIL (or check free-threading state)
// 2. Build Python request representation from req.method/path/headers/body
// 3. Call into turboAPI's existing Python dispatch (request_handler.py)
// 4. Receive 3-tuple (status, content_type, body) or
// 5-tuple (status, content_type, b"", iterator, headers) for streaming
// 5. Marshal back to nanoapi.Response (or to ResponseWriter for streaming)
// 6. Release GIL
}
```
Step 3 — replace server.zig accept loop
Currently `zig/src/main.zig` boots its own listener and dispatch. New shape:
```zig
const py_disp = try PyDispatcher.init(allocator, ...);
defer py_disp.deinit();
try nanoapi.server.serveGeneric(py_disp.dispatcher(), allocator, .{
.host = host,
.port = port,
.runtime = .auto, // io_uring on Linux, kqueue on macOS
.worker_threads = 0, // one worker per CPU
});
```
`zig/src/server.zig` (the 109KB file) gets deleted. Maybe keep multipart.zig if nanoapi's multipart isn't equivalent yet; verify with parity tests.
Step 4 — preserve the 5-tuple streaming path from #164
The existing streaming dispatch in `request_handler.py` returns either a 3-tuple (sync) or 5-tuple `(status, content_type, b"", iterator, headers)` (streaming). `PyDispatcher.handle` needs to detect the 5-tuple and use nanoapi's streaming primitives (`StreamingResponse` / `SseWriter` / `LLMStreamResponse`) instead of building a plain `Response`. May need a small `ResponseWriter` extension on the nanoapi side if the existing streaming-response API doesn't expose chunked-write directly enough — in which case file as a follow-up nanoapi issue.
Step 5 — preserve free-threading + dhi pre-GIL validation
The `py_atomic_shim.c` and `py_gil_shim.c` C files stay. dhi pre-GIL JSON validation runs before GIL acquisition in `PyDispatcher.handle`. The exact spot is the same as today's `zig/src/main.zig`.
Step 6 — turn parity green
Run turbohttp-parity against the ported turboAPI. Every test that was green on nanoapi should now be green on turboAPI too. CI gate the merge on PARITY.md being green.
Risks
Acceptance
Out of scope