feat: std.http pooled handler dispatch (PLAN-HTTPZ S7b)

thread_pool_count = 0 (default) keeps handlers inline on the loop
thread — the measured fast path (BENCH-HTTPZ.md). N > 0 dispatches
each parsed request to a std.thread Pool of N workers, completing the
httpz two-pool shape: the connection freezes as CONN_HANDLING (no
reads, growth, eviction, or recycling — the worker borrows views into
its read buffer), the worker runs the handler under a per-job arena
and serializes into job-owned bytes, the completion queues under the
PoolState mutex, and the loop wakes through the new std.event wake
channel (kqueue EVFILT_USER + EV_CLEAR; the epoll twin maps to
eventfd), attaches the response, compacts the buffer, and resumes
keep-alive/pipeline handling. A full backlog sheds with 503. Stale
completions (generation mismatch after close) are dropped. Pool mode
requires the server's constructing allocator to be thread-safe
(GPA/malloc), documented on the knob.

PoolState lives behind a heap pointer (it embeds a Mutex and is shared
with workers; the Server struct itself is returned by value).
serialize_response/run_handler_job share one serialize_bytes.

examples/1633 gains the pooled section (GET, body echo, 404 across
worker threads) plus the loop-wake path exercised end to end; AOT run
five times. examples/1632 unchanged but the Event struct gains `user`.
This commit is contained in:
agra
2026-06-12 22:31:27 +03:00
parent 7f23bb7530
commit e57a27205e
42 changed files with 95852 additions and 81382 deletions

View File

@@ -35,6 +35,7 @@ EventErr :: error {
// readable/writable — which direction is ready;
// eof — the peer finished writing (drain pending bytes, then close);
// err — the registration itself failed asynchronously;
// user — a cross-thread wake() (see add_wake), no fd attached;
// nbytes — bytes readable / writable-buffer space (backend estimate);
// udata — the word given at registration, verbatim.
Event :: struct {
@@ -44,6 +45,7 @@ Event :: struct {
writable: bool = false;
eof: bool = false;
err: bool = false;
user: bool = false; // a wake() delivery, not fd readiness
nbytes: i64 = 0;
}
@@ -76,6 +78,22 @@ Loop :: struct {
kqb.kq_apply(self.kq, kqb.kev_change(fd, kqb.EVFILT_WRITE, kqb.EV_DELETE, 0));
}
// Register the loop's wake channel: wake() from ANY thread makes
// wait() return an Event carrying `udata` with `.user` set. EV_CLEAR
// auto-resets, so one registration serves the loop's lifetime.
// (kqueue EVFILT_USER here; the epoll twin maps to eventfd.)
add_wake :: (self: *Loop, udata: usize) -> !EventErr {
ch : kqb.Kevent = .{ ident = 0, filter = kqb.EVFILT_USER, flags = kqb.EV_ADD | kqb.EV_CLEAR, udata = udata };
if !kqb.kq_apply(self.kq, ch) { raise error.Register; }
return;
}
// Thread-safe: kevent change submission is safe from any thread.
wake :: (self: *Loop) {
ch : kqb.Kevent = .{ ident = 0, filter = kqb.EVFILT_USER, fflags = kqb.NOTE_TRIGGER };
kqb.kq_apply(self.kq, ch);
}
// Fill `out` with ready events, waiting at most `timeout_ms`
// (negative = forever). Returns the count; 0 is a timeout.
wait :: (self: *Loop, out: []Event, timeout_ms: i64) -> (i64, !EventErr) {
@@ -90,6 +108,7 @@ Loop :: struct {
e : Event = .{ fd = xx ev.ident, udata = ev.udata, nbytes = ev.data };
if ev.filter == kqb.EVFILT_READ { e.readable = true; }
if ev.filter == kqb.EVFILT_WRITE { e.writable = true; }
if ev.filter == kqb.EVFILT_USER { e.user = true; }
if (ev.flags & kqb.EV_EOF) != 0 { e.eof = true; }
if (ev.flags & kqb.EV_ERROR) != 0 { e.err = true; }
out[i] = e;