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

@@ -39,18 +39,22 @@ contains :: (hay: string, needle: string) -> bool {
}
// Connect a nonblocking loopback client.
dial :: () -> i32 {
dial_port :: (port: i64) -> i32 {
fd := socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0);
if fd < 0 { return -1; }
addr : socket.SockAddr = .{
sin_len = 16, sin_family = xx socket.AF_INET,
sin_port = socket.htons(PORT), sin_addr = 0x0100007F,
sin_port = socket.htons(port), sin_addr = 0x0100007F,
};
if socket.connect(fd, @addr, 16) != 0 { socket.close(fd); return -1; }
if !socket.set_nonblocking(fd) { socket.close(fd); return -1; }
return fd;
}
dial :: () -> i32 {
return dial_port(PORT);
}
// True when `buf[0..len]` holds a complete response (headers + body).
resp_complete :: (buf: [*]u8, len: i64) -> bool {
s := string.{ ptr = buf, len = xx len };
@@ -210,6 +214,36 @@ main :: () -> i32 {
print("slow client evicted, healthy client served\n");
srv.close();
// ── pooled dispatch (S7b): same contract through worker threads ──
// thread_pool_count > 0 runs handlers on a pool; completions come
// back through the loop's wake channel. Same assertions: routing,
// body echo, keep-alive reuse — now crossing threads per request.
pcfg : http.Config = .{
port = PORT + 1,
timeout_request_ms = 1000,
timeout_keepalive_ms = 1000,
request_count = 50,
max_conn = 8,
thread_pool_count = 2,
thread_pool_backlog = 16,
};
psrv, pse := http.Server.init(pcfg, handler, 77);
if pse { print("pooled server init failed\n"); return 1; }
c5 := dial_port(PORT + 1);
if c5 < 0 { print("dial5 failed\n"); return 1; }
r7 := roundtrip(@psrv, c5, "GET /hello HTTP/1.1\r\nHost: t\r\n\r\n", @buf[0]);
if !contains(r7, "HTTP/1.1 200 OK") { print("pooled: bad status\n"); return 1; }
if !contains(r7, "hello GET") { print("pooled: bad body\n"); return 1; }
r8 := roundtrip(@psrv, c5, "POST /echo HTTP/1.1\r\nHost: t\r\nContent-Length: 9\r\n\r\nping-pong", @buf[0]);
if !contains(r8, "ping-pong") { print("pooled: echo failed\n"); return 1; }
r9 := roundtrip(@psrv, c5, "GET /missing HTTP/1.1\r\nHost: t\r\n\r\n", @buf[0]);
if !contains(r9, "HTTP/1.1 404 Not Found") { print("pooled: expected 404\n"); return 1; }
socket.close(c5);
psrv.close();
print("pooled dispatch: GET, echo, 404 across worker threads ok\n");
print("http server ok\n");
return 0;
}