distd serves through std.http — the readiness loop lands (PLAN-HTTPZ A1)

The hand-rolled sequential accept loop, its SO_RCVTIMEO band-aid, and
the whole src/server/http.sx module are retired: distd is now a
std.http handler. Server.init gets the store directory through the ctx
word; route() fills a Response instead of writing to a socket; every
handler ports mechanically (respond_error/load_or_503/respond_render
take *Response; bodies allocate from the per-request arena, never the
stack, since serialization happens after the handler returns).
Downloads keep X-Checksum-SHA256 via extra_headers; auth takes the
extracted Authorization value; the 411 contract (POST/PUT must declare
Content-Length) moves into the handler, pinned as before.

Config: 512 MiB read cap (whole-body artifact uploads), 120s request
deadline, 5s keepalive, 200 requests per connection. Idle connections
now cost nothing — timeouts evict, never block.

http_client gains its own 10s read timeout (the old shared helper's
secs->ms change had silently shrunk it to 10ms).

tests/server_http.sx pins the architecture: a request answers within
1s while SIX idle preconnects are held open (the retired loop paid
250ms-2s per idle socket serially), and two requests ride one
keep-alive connection. make test 24/24 green.
This commit is contained in:
agra
2026-06-12 22:00:31 +03:00
parent a6052bbb4c
commit 48a13c43ee
6 changed files with 243 additions and 496 deletions

View File

@@ -183,17 +183,28 @@ main :: () -> i32 {
process.assert(get_str(get_obj(bad, "error"), "code") == "download.unknown_object", "unknown digest names download.unknown_object");
print(" download ok\n");
// ── idle preconnect must not wedge the accept loop ────────────────
// Hold a connection open that never sends bytes (what a browser's
// speculative preconnect does) and require a real request to still be
// answered: the 2s read timeout must free the loop well inside curl's
// 5s budget. Pre-fix (no SO_RCVTIMEO) this curl times out with 000.
process.run("sh -c '(sleep 6 | nc 127.0.0.1 18792 > /dev/null 2>&1) &'");
// ── idle preconnects cost nothing (PLAN-HTTPZ A1) ─────────────────
// Hold SIX connections open that never send bytes (browser-style
// speculative preconnects) and require a real request to answer
// within 1s. The retired sequential loop paid its read timeout per
// idle socket serially (6 x 250ms band-aid = 1.5s; 6 x 2s = 12s
// before that), so this pins the readiness architecture, not a
// tuned timeout.
process.run("sh -c 'for i in 1 2 3 4 5 6; do (sleep 8 | nc 127.0.0.1 18792 > /dev/null 2>&1) & done'");
process.run("sleep 0.3");
wc := process.run(concat(concat("curl -s -m 5 -o /dev/null -w '%{http_code}' ", BASE), "/healthz"));
wc := process.run(concat(concat("curl -s -m 1 -o /dev/null -w '%{http_code}' ", BASE), "/healthz"));
process.assert(wc != null, "curl spawn failed (idle-conn case)");
process.assert(wc!.stdout == "200", "request must be served while an idle connection is held open");
print(" idle connection cannot wedge the loop\n");
process.assert(wc!.stdout == "200", "request must answer within 1s while 6 idle connections are held open");
print(" 6 idle preconnects: served within 1s\n");
// ── keep-alive: two requests ride one connection ──────────────────
// curl reuses its connection for consecutive URLs; both responses
// must arrive (the retired loop closed after every response).
ka := process.run(concat(concat(concat(concat("curl -s -m 2 ", BASE), "/healthz "), BASE), "/healthz"));
process.assert(ka != null, "curl spawn failed (keep-alive case)");
process.assert(ka!.stdout == "{\"status\":\"ok\"}{\"status\":\"ok\"}",
"two requests on one connection both answer");
print(" keep-alive reuse ok\n");
// ── freshness: publish B while the server runs ────────────────────
rb := process.run(publish_cmd(path_join(MDIR, "b.json")));