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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Windows uses overlapped sockets and an I/O completion port (IOCP).
Application callbacks can run on the I/O owner or on fixed startup workers.
Inline execution is the default for trusted bounded, nonblocking handlers;
blocking callbacks must explicitly select fixed startup workers.
Worker callbacks can [flush output without returning](docs/USING.md#flush-within-a-worker-callback).
The same reserved worker stack resumes after the owner completes transmission.
The [Windows receipt](reports/2026-09-06-windows-iocp.md) records native x64 evidence and its limits.
This is the M4 implementation informed by the adjacent
[Zig LLM Wiki](https://technologylab-ai.github.io/zigllmwiki/?page=wiki/bounded-http-server-design.md).
Expand Down
9 changes: 9 additions & 0 deletions docs/OWNERSHIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,19 @@ with release/acquire ordering. Wake signals are hints; the phase is authoritativ
| I/O receive/parse | I/O owner appends initialized receive bytes and advances the parser. One transport operation may borrow the destination. |
| Ready/running callback | Assigned worker borrows the immutable request and exclusively mutates its writer/state. The I/O owner can set the atomic cancellation flag and close networking, but cannot recycle the slot. |
| Published callback result | I/O owner acquires the committed writer snapshot and action; the worker stops accessing request/writer/state until another dispatch. |
| Worker flush (`stream_ready`/`stream_wait`) | The callback stack stays live. The owner acquires only the frozen output. The worker waits for output release before continuing. |
| Sending | I/O owner advances partial-send cursors. Committed payload storage stays immutable until every send using it completes. |
| Flushed | Writer storage is released/reset and the same handler is dispatched with `.flushed`; the request and continuation state remain borrowed. |
| Finished/closing | No continuation is promised. Slot storage is reused only after the callback has returned and all target/cancel completions have drained. |

`Context.flushAndWait()` retains the same worker callback across transmission.
Successful completion publishes `running` after restoring writable capacity.
Cancellation publishes `running` only after all kernel borrows end.
The worker then receives `Cancelled` and must return `.close`.
Only final callback return publishes `result` and permits eventual slot reuse.
Neither streaming phase ends the application borrow.
See [worker flush](USING.md#flush-within-a-worker-callback) for the public contract and worker occupancy limit.

The receive buffer stays at a stable address through parsing and all callbacks
for that request. `Request.method`, `target`, `headers`, `header(name)` results,
`body_wire` and each body-iterator span borrow it. Chunked decoding removes no
Expand Down
29 changes: 29 additions & 0 deletions docs/USING.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,35 @@ Return that result immediately.
The owner completes response framing without another callback for that response.
Finished responses can remain buffered within a batch before transmission.

### Flush within a worker callback

`Context.supportsBlockingFlush()` reports whether the callback runs on a fixed application worker.
`Context.flushAndWait()` sends the current snapshot without returning from that callback.
The method freezes output and waits on the worker's existing notification mechanism.
The I/O owner continues processing network events and other connections.
Successful completion restores writable capacity and resumes the same worker stack.
Application code can then write more output and flush again.
Return `writer.finish()` after the final write.

This operation requires `.workers` execution and a begun response without an outstanding reservation.
Inline callbacks receive `BlockingFlushUnavailable` before the writer changes.
Invalid writer state returns `InvalidState`.
Disconnect, deadline, shutdown, or response-limit failure can return `Cancelled`.
On cancellation, return `.close`; do not append output or reuse the frozen writer.
The original request deadline and cumulative `max_response_bytes` limit remain unchanged across waits.

One active callback occupies one preallocated worker throughout its flush waits and application work.
Other slots assigned to that worker wait for it to return.
There is no new request thread, heap allocation, or general asynchronous task scheduler.
Arbitrary application blocking still requires the existing external watchdog.

The context and writer stay on their original callback thread.
Cancellation waits for every kernel borrow before unwinding the waiting worker.
The slot remains retained until the callback also returns.
Successful flush proves local transmission completion, not peer application receipt.
HEAD suppresses payload transmission, and empty flushes do not end chunked responses.
The existing return-and-resume `Writer.flush()` API remains available.

### Capacity and `WouldBlock`

`Writer.capacity()` reports conservative body capacity after a flush.
Expand Down
66 changes: 66 additions & 0 deletions src/api.zig
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const assert = std.debug.assert;
pub const Action = enum { flush, finish, close };
pub const Event = enum { request, flushed };
pub const Handler = *const fn (*Context) Action;
pub const FlushError = error{ BlockingFlushUnavailable, InvalidState, Cancelled };

pub const Context = struct {
request: *const http.Request,
Expand All @@ -14,6 +15,31 @@ pub const Context = struct {
state: *[8]usize,
application: ?*anyopaque,
cancelled: *const std.atomic.Value(bool),
/// The scheduler supplies this hook only for an existing application worker.
blocking_flush: ?BlockingFlush = null,

pub const BlockingFlush = struct {
context: *anyopaque,
flush: *const fn (*anyopaque) FlushError!void,
};

pub fn supportsBlockingFlush(self: *const Context) bool {
return self.blocking_flush != null;
}

/// Send this snapshot and resume the same callback with fresh output capacity.
/// The worker waits; the I/O owner continues processing other connections.
/// Completion ends the local transport borrow, not the peer's processing.
/// After publication, cancellation waits for transport borrows before returning an error.
/// The context and writer must remain on their original callback thread.
pub fn flushAndWait(self: *Context) FlushError!void {
const hook = self.blocking_flush orelse return error.BlockingFlushUnavailable;
if (self.cancelled.load(.acquire)) return error.Cancelled;
if (self.writer.frozen or self.writer.reserved != 0 or !self.writer.began)
return error.InvalidState;
_ = self.writer.flush();
return hook.flush(hook.context);
}
};

/// Largest response head this framework generates, plus the chunk-size field.
Expand Down Expand Up @@ -444,6 +470,46 @@ fn testCache() HeaderCache {
return cache;
}

test "blocking flush rejects unavailable, cancelled and unpublished states without freezing" {
var arena: [1024]u8 = undefined;
const cache = testCache();
var writer = Writer.init(&arena, &cache, 0);
writer.open(0, true, false);
var request: http.Request = undefined;
var state: [8]usize = @splat(0);
var cancelled: std.atomic.Value(bool) = .init(false);
var called = false;
var context: Context = .{
.request = &request,
.writer = &writer,
.event = .request,
.state = &state,
.application = null,
.cancelled = &cancelled,
};
try std.testing.expect(!context.supportsBlockingFlush());
try std.testing.expectError(error.BlockingFlushUnavailable, context.flushAndWait());
context.blocking_flush = .{ .context = &called, .flush = struct {
fn flush(pointer: *anyopaque) FlushError!void {
const value: *bool = @ptrCast(@alignCast(pointer));
value.* = true;
}
}.flush };
try std.testing.expect(context.supportsBlockingFlush());
try std.testing.expectError(error.InvalidState, context.flushAndWait());
try writer.begin(200, "text/plain", null);
_ = try writer.reserve(1);
try std.testing.expectError(error.InvalidState, context.flushAndWait());
writer.commit(0);
cancelled.store(true, .release);
try std.testing.expectError(error.Cancelled, context.flushAndWait());
try std.testing.expect(!writer.frozen and !called);
cancelled.store(false, .release);
try context.flushAndWait();
try std.testing.expect(writer.frozen and called);
try std.testing.expectError(error.InvalidState, context.flushAndWait());
}

test "begin writes the head into the arena and flush keeps the snapshot" {
var arena: [1024]u8 = undefined;
const cache = testCache();
Expand Down
Loading