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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user