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

@@ -18,7 +18,20 @@
#import "modules/std.sx";
sock :: #import "modules/std/socket.sx";
hs :: #import "../server/http.sx";
// Bound blocking reads to `ms` milliseconds so a dead server fails the
// publish instead of hanging it (10s: LAN responses arrive in
// milliseconds; a large publish response can still take a moment).
HC_SO_RCVTIMEO :i32: 0x1006; // macOS
HcTimeval :: struct {
tv_sec: i64;
tv_usec: i32 = 0;
pad: i32 = 0;
}
hc_set_read_timeout :: (fd: i32, ms: i64) {
tv : HcTimeval = .{ tv_sec = ms / 1000, tv_usec = xx ((ms % 1000) * 1000) };
sock.setsockopt(fd, sock.SOL_SOCKET, HC_SO_RCVTIMEO, xx @tv, 16);
}
netc :: #library "c";
c_connect :: (fd: i32, addr: *sock.SockAddr, addrlen: u32) -> i32 #foreign netc "connect";
@@ -136,7 +149,7 @@ http_post :: (srv: ServerUrl, path: string, bearer: string, content_type: string
sin_port = sock.htons(srv.port), sin_addr = srv.addr,
};
if c_connect(fd, @addr, 16) < 0 { sock.close(fd); raise error.Connect; }
hs.set_read_timeout(fd, 10);
hc_set_read_timeout(fd, 10000);
h := concat("POST ", concat(path, " HTTP/1.1\r\n"));
h = concat(h, concat("Host: ", concat(srv.host, "\r\n")));

View File

@@ -41,7 +41,6 @@
#import "../domain/audit.sx";
#import "../domain/validate.sx";
#import "../repo/repo.sx";
http :: #import "http.sx";
db :: #import "../repo/db.sx";
jout :: #import "../json_out.sx";
pl :: #import "../publish/publish.sx";
@@ -149,33 +148,38 @@ adm_has_str :: (l: *List(string), s: string) -> bool {
// ── responses ─────────────────────────────────────────────────────────
adm_error :: (client: i32, code: i64, fail_code: string, fail_message: string) {
adm_error :: (resp: *http.Response, code: i64, fail_code: string, fail_message: string) {
f : jout.CliFailure = .{ code = fail_code, message = fail_message };
raw : [4096]u8 = ---;
// Allocated, not stack: the server serializes after the handler
// returns, so the body must outlive this frame.
raw : [*]u8 = xx context.allocator.alloc_bytes(4096);
werr := false;
n := jout.write_error(f, string.{ ptr = @raw[0], len = 4096 }) catch { werr = true; 0 };
n := jout.write_error(f, string.{ ptr = raw, len = 4096 }) catch { werr = true; 0 };
body := "{\"status\":\"error\"}";
if !werr { body = string.{ ptr = @raw[0], len = n }; }
http.respond(client, code, "application/json", "", body);
if !werr { body = string.{ ptr = raw, len = n }; }
resp.status = code;
resp.content_type = "application/json";
resp.body = body;
}
adm_load :: (client: i32, store_dir: string) -> ?Repo {
adm_load :: (resp: *http.Response, store_dir: string) -> ?Repo {
if !db.store_exists(store_dir) {
adm_error(client, 503, "store.load",
adm_error(resp, 503, "store.load",
concat("no store database (nothing published yet): ", store_dir));
return null;
}
loaded, le := db.load(store_dir);
if le {
adm_error(client, 503, "store.load",
adm_error(resp, 503, "store.load",
concat("the store database could not be loaded: ", store_dir));
return null;
}
return loaded;
}
adm_html :: (client: i32, page: string) {
http.respond(client, 200, "text/html; charset=utf-8", "", page);
adm_html :: (resp: *http.Response, page: string) {
resp.content_type = "text/html; charset=utf-8";
resp.body = page;
}
// ── chrome ────────────────────────────────────────────────────────────
@@ -512,44 +516,44 @@ render_admin_audit :: (repo: *Repo) -> string {
// ── routing (distd delegates every /admin path here) ─────────────────
handle_admin :: (client: i32, store_dir: string, path: string) {
rq := adm_load(client, store_dir);
handle_admin :: (store_dir: string, path: string, resp: *http.Response) {
rq := adm_load(resp, store_dir);
if rq == null { return; }
repo := rq!;
if path == "/admin" or path == "/admin/" {
adm_html(client, render_admin_apps(@repo));
adm_html(resp, render_admin_apps(@repo));
return;
}
if path == "/admin/tokens" {
adm_html(client, render_admin_tokens(@repo));
adm_html(resp, render_admin_tokens(@repo));
return;
}
if path == "/admin/audit" {
adm_html(client, render_admin_audit(@repo));
adm_html(resp, render_admin_audit(@repo));
return;
}
if adm_starts_with(path, "/admin/apps/") {
slug := adm_tail(path, "/admin/apps/");
aq := repo.find_app_by_slug(slug);
if aq == null {
adm_error(client, 404, "admin.unknown_app",
adm_error(resp, 404, "admin.unknown_app",
concat("no app with that slug in the store: ", slug));
return;
}
adm_html(client, render_admin_app(@repo, aq!));
adm_html(resp, render_admin_app(@repo, aq!));
return;
}
if adm_starts_with(path, "/admin/releases/") {
id := adm_tail(path, "/admin/releases/");
relq := repo.get_release(id);
if relq == null {
adm_error(client, 404, "admin.unknown_release",
adm_error(resp, 404, "admin.unknown_release",
concat("no release with that id in the store: ", id));
return;
}
adm_html(client, render_admin_release(@repo, relq!));
adm_html(resp, render_admin_release(@repo, relq!));
return;
}
adm_error(client, 404, "http.not_found", concat("no admin route for ", path));
adm_error(resp, 404, "http.not_found", concat("no admin route for ", path));
}

View File

@@ -2,7 +2,7 @@
// auth.sx — bearer-token authentication for distd's write surface
// (subplan 04, Slice 2).
//
// `authenticate` takes the raw header block plus the (scope, app, channel)
// `authenticate` takes the Authorization header VALUE plus the (scope, app, channel)
// the operation demands, and either returns the live Token or refuses:
//
// 401 Unauthorized — no `Authorization` header, a non-Bearer scheme, or
@@ -36,7 +36,6 @@ db :: #import "../repo/db.sx";
jout :: #import "../json_out.sx";
pl :: #import "../publish/publish.sx";
tops :: #import "../token/ops.sx";
http :: #import "http.sx";
AuthErr :: error {
Unauthorized, // -> 401
@@ -79,14 +78,13 @@ check_message :: (e: TokenCheckErr, scope: string, app_slug: string, channel: st
// Authenticate the request these headers came with, demanding `scope`
// against (`app_slug`, `channel`) — "" where the operation has no app or
// channel to constrain.
authenticate :: (store_dir: string, headers: string, scope: string, app_slug: string, channel: string, fail_out: *jout.CliFailure) -> (Token, !AuthErr) {
hq := http.header_value(headers, "authorization");
if hq == null {
authenticate :: (store_dir: string, auth_header: string, scope: string, app_slug: string, channel: string, fail_out: *jout.CliFailure) -> (Token, !AuthErr) {
if auth_header.len == 0 {
fail_out.code = "auth.missing";
fail_out.message = "this route requires Authorization: Bearer <token>";
raise error.Unauthorized;
}
sq := bearer_secret(hq!);
sq := bearer_secret(auth_header);
if sq == null {
fail_out.code = "auth.malformed";
fail_out.message = "Authorization header is not a Bearer credential";

View File

@@ -62,7 +62,6 @@
#import "../validation/artifact_file.sx";
#import "../publish/publish.sx";
sock :: #import "modules/std/socket.sx";
http :: #import "http.sx";
au :: #import "auth.sx";
db :: #import "../repo/db.sx";
jout :: #import "../json_out.sx";
@@ -126,29 +125,34 @@ render_buf :: () -> string {
// ── responses ─────────────────────────────────────────────────────────
// JSON error body in the CLI's error shape, sent with the HTTP status.
respond_error :: (client: i32, code: i64, fail_code: string, fail_message: string) {
respond_error :: (resp: *http.Response, code: i64, fail_code: string, fail_message: string) {
f : jout.CliFailure = .{ code = fail_code, message = fail_message };
raw : [4096]u8 = ---;
// The body must outlive this frame (the server serializes after the
// handler returns), so the render buffer comes from the per-request
// arena, never the stack.
raw : [*]u8 = xx context.allocator.alloc_bytes(4096);
werr := false;
n := jout.write_error(f, string.{ ptr = @raw[0], len = 4096 }) catch { werr = true; 0 };
n := jout.write_error(f, string.{ ptr = raw, len = 4096 }) catch { werr = true; 0 };
body := "{\"status\":\"error\"}";
if !werr { body = string.{ ptr = @raw[0], len = n }; }
http.respond(client, code, "application/json", "", body);
if !werr { body = string.{ ptr = raw, len = n }; }
resp.status = code;
resp.content_type = "application/json";
resp.body = body;
}
// ── /api renders (builders own the `try`, callers catch) ─────────────
// Reload the persisted model. Null means the store has no readable
// database — the 503 error response has already been sent.
load_or_503 :: (client: i32, store_dir: string) -> ?Repo {
load_or_503 :: (resp: *http.Response, store_dir: string) -> ?Repo {
if !db.store_exists(store_dir) {
respond_error(client, 503, "store.load",
respond_error(resp, 503, "store.load",
concat("no store database (nothing published yet): ", store_dir));
return null;
}
loaded, le := db.load(store_dir);
if le {
respond_error(client, 503, "store.load",
respond_error(resp, 503, "store.load",
concat("the store database could not be loaded: ", store_dir));
return null;
}
@@ -202,13 +206,15 @@ render_app_detail_json :: (repo: *Repo, app: App, dst: []u8) -> (i64, !JsonError
// Send `n` rendered bytes of `buf` as 200 JSON — or the overflow error
// when the render didn't fit RENDER_CAP.
respond_render :: (client: i32, buf: string, n: i64, overflowed: bool) {
respond_render :: (resp: *http.Response, buf: string, n: i64, overflowed: bool) {
if overflowed {
respond_error(client, 500, "http.response_overflow",
respond_error(resp, 500, "http.response_overflow",
"response exceeded the server's render buffer");
return;
}
http.respond(client, 200, "application/json", "", string.{ ptr = buf.ptr, len = n });
resp.status = 200;
resp.content_type = "application/json";
resp.body = string.{ ptr = buf.ptr, len = n };
}
// ── HTML index (the install-page seed, subplan 04 Slice 5) ───────────
@@ -302,34 +308,35 @@ render_index :: (repo: *Repo) -> string {
return concat(page, INDEX_FOOT);
}
handle_index :: (client: i32, store_dir: string) {
rq := load_or_503(client, store_dir);
handle_index :: (resp: *http.Response, store_dir: string) {
rq := load_or_503(resp, store_dir);
if rq == null { return; }
repo := rq!;
http.respond(client, 200, "text/html; charset=utf-8", "", render_index(@repo));
resp.content_type = "text/html; charset=utf-8";
resp.body = render_index(@repo);
}
// ── routes ────────────────────────────────────────────────────────────
handle_apps_index :: (client: i32, store_dir: string) {
rq := load_or_503(client, store_dir);
handle_apps_index :: (resp: *http.Response, store_dir: string) {
rq := load_or_503(resp, store_dir);
if rq == null { return; }
repo := rq!;
buf := render_buf();
werr := false;
n := render_apps_json(@repo, buf) catch { werr = true; 0 };
respond_render(client, buf, n, werr);
respond_render(resp, buf, n, werr);
}
handle_app_detail :: (client: i32, store_dir: string, slug: string) {
rq := load_or_503(client, store_dir);
handle_app_detail :: (resp: *http.Response, store_dir: string, slug: string) {
rq := load_or_503(resp, store_dir);
if rq == null { return; }
repo := rq!;
aq := repo.find_app_by_slug(slug);
if aq == null {
respond_error(client, 404, "api.unknown_app",
respond_error(resp, 404, "api.unknown_app",
concat("no app with that slug in the store: ", slug));
return;
}
@@ -337,24 +344,25 @@ handle_app_detail :: (client: i32, store_dir: string, slug: string) {
buf := render_buf();
werr := false;
n := render_app_detail_json(@repo, aq!, buf) catch { werr = true; 0 };
respond_render(client, buf, n, werr);
respond_render(resp, buf, n, werr);
}
handle_download :: (client: i32, store_dir: string, sha: string) {
handle_download :: (resp: *http.Response, store_dir: string, sha: string) {
if !is_hex64(sha) {
respond_error(client, 404, "download.bad_key",
respond_error(resp, 404, "download.bad_key",
"download key must be a 64-char lowercase-hex sha256");
return;
}
opath := path_join(store_dir, concat("objects/", sha));
bq := read_file(opath);
if bq == null {
respond_error(client, 404, "download.unknown_object",
respond_error(resp, 404, "download.unknown_object",
concat("no object with that digest in the store: ", sha));
return;
}
extra := concat("X-Checksum-SHA256: ", concat(sha, "\r\n"));
http.respond(client, 200, "application/octet-stream", extra, bq!);
resp.content_type = "application/octet-stream";
resp.extra_headers = concat("X-Checksum-SHA256: ", concat(sha, "\r\n"));
resp.body = bq!;
}
// ── install pages (subplan 04 Slice 5) ────────────────────────────────
@@ -408,23 +416,23 @@ InstallCtx :: struct {
host: string = "localhost";
}
resolve_install :: (client: i32, repo: *Repo, slug: string, chan_name: string) -> ?InstallCtx {
resolve_install :: (resp: *http.Response, repo: *Repo, slug: string, chan_name: string) -> ?InstallCtx {
aq := repo.find_app_by_slug(slug);
if aq == null {
respond_error(client, 404, "install.unknown_app",
respond_error(resp, 404, "install.unknown_app",
concat("no app with that slug in the store: ", slug));
return null;
}
app := aq!;
cq := repo.get_channel(app.id, chan_name);
if cq == null {
respond_error(client, 404, "install.unknown_channel",
respond_error(resp, 404, "install.unknown_channel",
concat("the app has no channel with that name: ", chan_name));
return null;
}
rq := repo.get_release(cq!.current_release_id);
if rq == null {
respond_error(client, 404, "install.no_release",
respond_error(resp, 404, "install.no_release",
concat("the channel does not point at a published release: ", chan_name));
return null;
}
@@ -528,43 +536,41 @@ render_install :: (ctx: *InstallCtx, repo: *Repo, detected: ?Platform) -> string
return concat(page, "</body></html>");
}
handle_install_page :: (client: i32, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
rq := load_or_503(client, store_dir);
handle_install_page :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
rq := load_or_503(resp, store_dir);
if rq == null { return; }
repo := rq!;
ctxq := resolve_install(client, @repo, slug, chan_name);
ctxq := resolve_install(resp, @repo, slug, chan_name);
if ctxq == null { return; }
ctx := ctxq!;
ua := "";
uq := http.header_value(req.headers, "user-agent");
if uq != null { ua = uq!; }
hostq := http.header_value(req.headers, "host");
if hostq != null { ctx.host = hostq!; }
ua := http.find_header(req, "user-agent");
hostv := http.find_header(req, "host");
if hostv.len > 0 { ctx.host = hostv; }
http.respond(client, 200, "text/html; charset=utf-8", "",
render_install(@ctx, @repo, ua_platform(ua)));
resp.content_type = "text/html; charset=utf-8";
resp.body = render_install(@ctx, @repo, ua_platform(ua));
}
// The enterprise OTA manifest: Apple's plist shape, with the software
// package URL pointing back at this server over HTTPS (itms-services
// refuses plain http; TLS terminates at the reverse proxy).
handle_manifest_plist :: (client: i32, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
rq := load_or_503(client, store_dir);
handle_manifest_plist :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
rq := load_or_503(resp, store_dir);
if rq == null { return; }
repo := rq!;
ctxq := resolve_install(client, @repo, slug, chan_name);
ctxq := resolve_install(resp, @repo, slug, chan_name);
if ctxq == null { return; }
ctx := ctxq!;
if ctx.app.ios_mode != .enterprise {
respond_error(client, 404, "install.not_enterprise",
respond_error(resp, 404, "install.not_enterprise",
"the app's iOS install mode is not enterprise; no OTA manifest exists");
return;
}
bid := bundle_id_for(ctx.app, .ios);
if bid.len == 0 {
respond_error(client, 404, "install.no_bundle_id",
respond_error(resp, 404, "install.no_bundle_id",
"the app has no iOS bundle id; set one with: dist app set --ios-bundle-id");
return;
}
@@ -578,14 +584,14 @@ handle_manifest_plist :: (client: i32, store_dir: string, req: *http.Request, sl
ai += 1;
}
if !found {
respond_error(client, 404, "install.no_ios_artifact",
respond_error(resp, 404, "install.no_ios_artifact",
"the channel's current release carries no iOS artifact");
return;
}
host := "localhost";
hostq := http.header_value(req.headers, "host");
if hostq != null { host = hostq!; }
hostv2 := http.find_header(req, "host");
if hostv2.len > 0 { host = hostv2; }
url := concat("https://", concat(host, concat("/download/", art.sha256)));
x := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\"><dict><key>items</key><array><dict>";
@@ -599,27 +605,28 @@ handle_manifest_plist :: (client: i32, store_dir: string, req: *http.Request, sl
x = concat(x, html_escape(ctx.app.display_name));
x = concat(x, "</string></dict></dict></array></dict></plist>\n");
http.respond(client, 200, "application/xml", "", x);
resp.content_type = "application/xml";
resp.body = x;
}
// GET /install/<slug>/<channel>[/manifest.plist]
handle_install_route :: (client: i32, store_dir: string, req: *http.Request, tail: string) {
handle_install_route :: (resp: *http.Response, store_dir: string, req: *http.Request, tail: string) {
s1 := seg_split(tail);
if s1.head.len == 0 or s1.rest.len == 0 {
respond_error(client, 404, "http.not_found",
respond_error(resp, 404, "http.not_found",
"install pages live at /install/<slug>/<channel>");
return;
}
s2 := seg_split(s1.rest);
if s2.rest.len == 0 {
handle_install_page(client, store_dir, req, s1.head, s2.head);
handle_install_page(resp, store_dir, req, s1.head, s2.head);
return;
}
if s2.rest == "manifest.plist" {
handle_manifest_plist(client, store_dir, req, s1.head, s2.head);
handle_manifest_plist(resp, store_dir, req, s1.head, s2.head);
return;
}
respond_error(client, 404, "http.not_found",
respond_error(resp, 404, "http.not_found",
concat("no install route for ", tail));
}
@@ -648,28 +655,28 @@ seg_split :: (s: string) -> Seg {
// Authenticate a write request or answer it. Null means the refusal
// response has already been sent.
auth_or_respond :: (client: i32, store_dir: string, req: *http.Request, app_slug: string, channel: string) -> ?Token {
auth_or_respond :: (resp: *http.Response, store_dir: string, req: *http.Request, app_slug: string, channel: string) -> ?Token {
fail : jout.CliFailure = .{};
t, ae := au.authenticate(store_dir, req.headers, "publish", app_slug, channel, @fail);
t, ae := au.authenticate(store_dir, http.find_header(req, "authorization"), "publish", app_slug, channel, @fail);
if ae {
status : i64 = 401;
if ae == error.Forbidden { status = 403; }
if ae == error.Unavailable { status = 503; }
respond_error(client, status, fail.code, fail.message);
respond_error(resp, status, fail.code, fail.message);
return null;
}
return t;
}
// JSON-object body, or null after answering 400.
body_object :: (client: i32, body: string) -> ?Object {
body_object :: (resp: *http.Response, body: string) -> ?Object {
v, pe := jsrv.parse(body, context.allocator);
if pe {
respond_error(client, 400, "api.bad_json", "request body is not valid JSON");
respond_error(resp, 400, "api.bad_json", "request body is not valid JSON");
return null;
}
if v != .object {
respond_error(client, 400, "api.bad_json", "request body must be a JSON object");
respond_error(resp, 400, "api.bad_json", "request body must be a JSON object");
return null;
}
return v.object;
@@ -685,7 +692,7 @@ wb_find :: (o: Object, key: string) -> ?Value {
}
// Required string member, or null after answering 400.
wb_req_str :: (client: i32, o: Object, key: string) -> ?string {
wb_req_str :: (resp: *http.Response, o: Object, key: string) -> ?string {
vq := wb_find(o, key);
if vq != null {
v := vq!;
@@ -693,7 +700,7 @@ wb_req_str :: (client: i32, o: Object, key: string) -> ?string {
if v.str.len > 0 { return v.str; }
}
}
respond_error(client, 400, "api.missing_field",
respond_error(resp, 400, "api.missing_field",
concat("body requires a non-empty string member: ", key));
return null;
}
@@ -717,10 +724,10 @@ wb_opt_int :: (o: Object, key: string, default_: i64) -> i64 {
// POST /api/upload — content-address the raw body into the store. The
// upload is app-agnostic (the bytes are inert until a release references
// them), so auth demands only the publish scope.
handle_upload :: (client: i32, store_dir: string, req: *http.Request) {
if auth_or_respond(client, store_dir, req, "", "") == null { return; }
handle_upload :: (resp: *http.Response, store_dir: string, req: *http.Request) {
if auth_or_respond(resp, store_dir, req, "", "") == null { return; }
if req.body.len == 0 {
respond_error(client, 400, "upload.empty", "upload body is empty");
respond_error(resp, 400, "upload.empty", "upload body is empty");
return;
}
st := Store.init(store_dir);
@@ -730,31 +737,31 @@ handle_upload :: (client: i32, store_dir: string, req: *http.Request) {
if se { werr = true; }
if !se { key = k; }
if werr {
respond_error(client, 500, "store.write",
respond_error(resp, 500, "store.write",
"upload bytes could not be content-addressed into the store");
return;
}
body := concat("{\"status\":\"stored\",\"sha256\":\"", concat(key, concat("\",\"size_bytes\":", concat(int_to_string(req.body.len), "}"))));
http.respond(client, 200, "application/json", "", body);
resp.content_type = "application/json";
resp.body = concat("{\"status\":\"stored\",\"sha256\":\"", concat(key, concat("\",\"size_bytes\":", concat(int_to_string(req.body.len), "}"))));
}
// POST /api/apps/<slug>/releases — publish a release whose artifacts name
// already-uploaded objects by digest. Mirrors the CLI manifest checks
// (platform, digest, size, content type, filename extension) against the
// STORE rather than local files, then commits through the shared pipeline.
handle_release_create :: (client: i32, store_dir: string, req: *http.Request, slug: string) {
oq := body_object(client, req.body);
handle_release_create :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string) {
oq := body_object(resp, req.body);
if oq == null { return; }
o := oq!;
versionq := wb_req_str(client, o, "version");
versionq := wb_req_str(resp, o, "version");
if versionq == null { return; }
version := versionq!;
channelq := wb_req_str(client, o, "channel");
channelq := wb_req_str(resp, o, "channel");
if channelq == null { return; }
channel := channelq!;
tokq := auth_or_respond(client, store_dir, req, slug, channel);
tokq := auth_or_respond(resp, store_dir, req, slug, channel);
if tokq == null { return; }
tok := tokq!;
@@ -768,7 +775,7 @@ handle_release_create :: (client: i32, store_dir: string, req: *http.Request, sl
}
}
if !arts_ok {
respond_error(client, 400, "api.missing_field",
respond_error(resp, 400, "api.missing_field",
"body requires a non-empty artifacts array");
return;
}
@@ -778,25 +785,25 @@ handle_release_create :: (client: i32, store_dir: string, req: *http.Request, sl
i := 0;
while i < arts_arr.len {
if arts_arr.items[i] != .object {
respond_error(client, 400, "api.bad_json", "each artifact must be a JSON object");
respond_error(resp, 400, "api.bad_json", "each artifact must be a JSON object");
return;
}
ao := arts_arr.items[i].object;
pidq := wb_req_str(client, ao, "platform");
pidq := wb_req_str(resp, ao, "platform");
if pidq == null { return; }
platform, perr := parse_platform(pidq!);
if perr {
respond_error(client, 400, "api.unknown_platform",
respond_error(resp, 400, "api.unknown_platform",
concat("unknown platform id: ", pidq!));
return;
}
shaq := wb_req_str(client, ao, "sha256");
shaq := wb_req_str(resp, ao, "sha256");
if shaq == null { return; }
sha := shaq!;
if !is_hex64(sha) {
respond_error(client, 400, "api.bad_digest",
respond_error(resp, 400, "api.bad_digest",
"artifact sha256 must be a 64-char lowercase-hex digest");
return;
}
@@ -806,14 +813,14 @@ handle_release_create :: (client: i32, store_dir: string, req: *http.Request, sl
opath := path_join(store_dir, concat("objects/", sha));
ob := read_file(opath);
if ob == null {
respond_error(client, 404, "api.unknown_object",
respond_error(resp, 404, "api.unknown_object",
concat("no object with that digest in the store (upload it first): ", sha));
return;
}
actual_size := ob!.len;
declared := wb_opt_int(ao, "size_bytes", -1);
if declared >= 0 and declared != actual_size {
respond_error(client, 400, "api.size_mismatch",
respond_error(resp, 400, "api.size_mismatch",
concat("declared size_bytes does not match the stored object: ", sha));
return;
}
@@ -821,7 +828,7 @@ handle_release_create :: (client: i32, store_dir: string, req: *http.Request, sl
ct := wb_opt_str(ao, "content_type");
if ct.len == 0 { ct = pl.default_content_type(platform); }
if !is_allowed_content_type(ct) {
respond_error(client, 400, "api.content_type_denied",
respond_error(resp, 400, "api.content_type_denied",
concat("artifact content type is not on the allow-list: ", ct));
return;
}
@@ -831,7 +838,7 @@ handle_release_create :: (client: i32, store_dir: string, req: *http.Request, sl
fname = concat(slug, concat("-", concat(version, concat(".", expected_ext(platform)))));
}
if !ext_matches_platform(fname, platform) {
respond_error(client, 400, "api.extension_mismatch",
respond_error(resp, 400, "api.extension_mismatch",
concat("artifact filename extension does not match its platform: ", fname));
return;
}
@@ -857,14 +864,14 @@ handle_release_create :: (client: i32, store_dir: string, req: *http.Request, sl
if ce == error.Persist {
if fail.code == "persist.load" { status = 503; }
}
respond_error(client, status, fail.code, fail.message);
respond_error(resp, status, fail.code, fail.message);
return;
}
if !ce {
buf := render_buf();
werr := false;
n := pl.write_json(@outcome, buf) catch { werr = true; 0 };
respond_render(client, buf, n, werr);
respond_render(resp, buf, n, werr);
}
}
@@ -878,71 +885,71 @@ op_http_status :: (e: ops.OpError) -> i64 {
// POST /api/apps/<slug>/channels/<name>/promote — body {"release_id":..};
// delegates to the CLI's promote pipeline.
handle_promote :: (client: i32, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
oq := body_object(client, req.body);
handle_promote :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
oq := body_object(resp, req.body);
if oq == null { return; }
relq := wb_req_str(client, oq!, "release_id");
relq := wb_req_str(resp, oq!, "release_id");
if relq == null { return; }
if auth_or_respond(client, store_dir, req, slug, chan_name) == null { return; }
if auth_or_respond(resp, store_dir, req, slug, chan_name) == null { return; }
fail : jout.CliFailure = .{};
o, e := ops.run_promote(store_dir, slug, chan_name, relq!, @fail);
if e {
respond_error(client, op_http_status(e), fail.code, fail.message);
respond_error(resp, op_http_status(e), fail.code, fail.message);
return;
}
if !e {
buf := render_buf();
werr := false;
n := ops.write_promote_json(@o, buf) catch { werr = true; 0 };
respond_render(client, buf, n, werr);
respond_render(resp, buf, n, werr);
}
}
// POST /api/apps/<slug>/channels/<name>/rollback — empty body; delegates
// to the CLI's rollback pipeline.
handle_rollback :: (client: i32, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
if auth_or_respond(client, store_dir, req, slug, chan_name) == null { return; }
handle_rollback :: (resp: *http.Response, store_dir: string, req: *http.Request, slug: string, chan_name: string) {
if auth_or_respond(resp, store_dir, req, slug, chan_name) == null { return; }
fail : jout.CliFailure = .{};
o, e := ops.run_rollback(store_dir, slug, chan_name, @fail);
if e {
respond_error(client, op_http_status(e), fail.code, fail.message);
respond_error(resp, op_http_status(e), fail.code, fail.message);
return;
}
if !e {
buf := render_buf();
werr := false;
n := ops.write_rollback_json(@o, buf) catch { werr = true; 0 };
respond_render(client, buf, n, werr);
respond_render(resp, buf, n, werr);
}
}
// Route a write request under POST /api/. Returns false when no write
// route matches (the caller 404s).
route_post :: (client: i32, store_dir: string, req: *http.Request) -> bool {
route_post :: (resp: *http.Response, store_dir: string, req: *http.Request) -> bool {
path := req.path;
if path == "/api/upload" {
handle_upload(client, store_dir, req);
handle_upload(resp, store_dir, req);
return true;
}
if starts_with(path, "/api/apps/") {
s1 := seg_split(tail_after(path, "/api/apps/"));
if s1.head.len == 0 { return false; }
if s1.rest == "releases" {
handle_release_create(client, store_dir, req, s1.head);
handle_release_create(resp, store_dir, req, s1.head);
return true;
}
if starts_with(s1.rest, "channels/") {
s2 := seg_split(tail_after(s1.rest, "channels/"));
if s2.head.len == 0 { return false; }
if s2.rest == "promote" {
handle_promote(client, store_dir, req, s1.head, s2.head);
handle_promote(resp, store_dir, req, s1.head, s2.head);
return true;
}
if s2.rest == "rollback" {
handle_rollback(client, store_dir, req, s1.head, s2.head);
handle_rollback(resp, store_dir, req, s1.head, s2.head);
return true;
}
}
@@ -950,117 +957,103 @@ route_post :: (client: i32, store_dir: string, req: *http.Request) -> bool {
return false;
}
// Route one parsed request: GET reads, POST writes.
route :: (client: i32, store_dir: string, req: *http.Request) -> i64 {
// Route one parsed request into `resp`: GET reads, POST writes. The
// outcome status lives in resp.status (respond_error sets it on every
// refusal path).
route :: (store_dir: string, req: *http.Request, resp: *http.Response) {
method := req.method;
path := req.path;
if method == "GET" {
if path == "/" {
handle_index(client, store_dir);
return 200;
handle_index(resp, store_dir);
return;
}
if path == "/healthz" {
http.respond(client, 200, "application/json", "", "{\"status\":\"ok\"}");
return 200;
resp.content_type = "application/json";
resp.body = "{\"status\":\"ok\"}";
return;
}
if path == "/api/apps" {
handle_apps_index(client, store_dir);
return 200;
handle_apps_index(resp, store_dir);
return;
}
if starts_with(path, "/api/apps/") {
handle_app_detail(client, store_dir, tail_after(path, "/api/apps/"));
return 200;
handle_app_detail(resp, store_dir, tail_after(path, "/api/apps/"));
return;
}
if starts_with(path, "/download/") {
handle_download(client, store_dir, tail_after(path, "/download/"));
return 200;
handle_download(resp, store_dir, tail_after(path, "/download/"));
return;
}
if starts_with(path, "/install/") {
handle_install_route(client, store_dir, req, tail_after(path, "/install/"));
return 200;
handle_install_route(resp, store_dir, req, tail_after(path, "/install/"));
return;
}
if path == "/admin" or starts_with(path, "/admin/") {
adm.handle_admin(client, store_dir, path);
return 200;
adm.handle_admin(store_dir, path, resp);
return;
}
respond_error(client, 404, "http.not_found",
respond_error(resp, 404, "http.not_found",
concat("no route for ", path));
return 404;
}
if method == "POST" {
if route_post(client, store_dir, req) { return 200; }
respond_error(client, 404, "http.not_found",
concat("no write route for ", path));
return 404;
}
respond_error(client, 405, "http.method_not_allowed",
"distd routes are GET (reads) or POST (token-gated writes)");
return 405;
}
// ── server loop ───────────────────────────────────────────────────────
// Read one request off `client`, route it, log the result line. All
// allocations land in the pushed per-request context allocator.
serve_one :: (client: i32, store_dir: string) {
req : http.Request = .{};
closed := false;
rstatus : i64 = 0;
rcode := "";
rmsg := "";
http.read_request(client, @req) catch (e) {
if e == error.Closed { closed = true; }
if !closed {
rstatus = 400; rcode = "http.bad_request";
rmsg = "request could not be read as HTTP (bad request line or truncated body)";
if e == error.LengthRequired {
rstatus = 411; rcode = "http.length_required";
rmsg = "POST/PUT requires a Content-Length header";
}
if e == error.BodyTooLarge {
rstatus = 413; rcode = "http.body_too_large";
rmsg = "request body exceeds the server's size cap";
}
if e == error.HeadersTooLarge {
rstatus = 400; rcode = "http.headers_too_large";
rmsg = "request header block exceeds 8K";
}
}
};
if closed { return; }
if rstatus > 0 {
respond_error(client, rstatus, rcode, rmsg);
slog(concat("distd: unreadable request -> ", concat(int_to_string(rstatus), "\n")));
return;
}
if method == "POST" {
if route_post(resp, store_dir, req) { return; }
respond_error(resp, 404, "http.not_found",
concat("no write route for ", path));
return;
}
respond_error(resp, 405, "http.method_not_allowed",
"distd routes are GET (reads) or POST (token-gated writes)");
}
code := route(client, store_dir, @req);
line := concat("distd: ", concat(req.method, concat(" ", concat(req.path, concat(" -> ", concat(int_to_string(code), "\n"))))));
// ── server loop (std.http, PLAN-HTTPZ A1) ─────────────────────────────
// The std.http handler: thread the store directory through the ctx
// word, keep the 411 contract (POST/PUT must declare Content-Length —
// std.http treats an absent header as a zero-length body), route, log.
// Per-request allocations land in std.http's per-dispatch arena.
DistdCtx :: struct {
store_dir: string;
}
distd_handle :: (req: *http.Request, resp: *http.Response, ctx: usize) {
dctx : *DistdCtx = xx ctx;
store_dir := dctx.store_dir;
refused := false;
if req.method == "POST" or req.method == "PUT" {
if http.find_header(req, "content-length").len == 0 {
respond_error(resp, 411, "http.length_required",
"POST/PUT requires a Content-Length header");
refused = true;
}
}
if !refused {
route(store_dir, req, resp);
}
line := concat("distd: ", concat(req.method, concat(" ", concat(req.path, concat(" -> ", concat(int_to_string(resp.status), "\n"))))));
slog(line);
}
// Bind 0.0.0.0:<port> and serve forever (sequential, one connection at a
// time — the deployment story is LAN/NAS-scale). Returns only when the
// socket can't be opened. Per-request allocations live in an arena that
// dies with the request.
// Serve the store over std.http's readiness loop (PLAN-HTTPZ A1): idle
// connections cost nothing, timeouts evict instead of blocking,
// keep-alive holds between requests. Returns only when the socket
// can't be opened.
run_server :: (store_dir: string, port: i64) -> !ServeErr {
fd, fe := http.listen_on(port);
if fe { raise error.Bind; }
dctx : DistdCtx = .{ store_dir = store_dir };
cfg : http.Config = .{
port = port,
max_conn = 256,
read_buf_cap = 536870912, // 512 MiB: artifact uploads arrive whole-body
timeout_request_ms = 120000, // a large upload must complete within this
timeout_keepalive_ms = 5000,
request_count = 200,
};
srv, se := http.Server.init(cfg, distd_handle, xx @dctx);
if se { raise error.Bind; }
slog(concat("distd: serving store ", concat(store_dir, concat(" on http://0.0.0.0:", concat(int_to_string(port), "\n")))));
while true {
client := sock.accept(fd, null, null);
if client < 0 { continue; }
http.set_read_timeout(client, 250);
gpa := GPA.init();
arena := Arena.init(xx gpa, 65536);
push Context.{ allocator = xx arena } {
serve_one(client, store_dir);
}
arena.deinit();
sock.close(client);
}
srv.run();
return;
}

View File

@@ -1,272 +0,0 @@
// =====================================================================
// http.sx — minimal HTTP/1.1 over `std.socket` (subplan 04, Slices 1+2).
//
// The temporary in-repo boundary for the missing `std.http`, written so it
// can be lifted into the sx stdlib later. Deliberately minimal — exactly
// what a JSON API + artifact upload/download server needs:
//
// * `listen_on(port)` — a listening TCP socket on 0.0.0.0:<port>
// (INADDR_ANY, so the server is reachable from the LAN);
// * `read_request` — one full request: request line, header block, and a
// Content-Length-bounded body, with typed failures for every refusal;
// * `header_value` — case-insensitive header lookup;
// * `respond` — one full response: status line, Content-Type/-Length,
// `Connection: close`, optional extra header lines, body.
//
// REQUEST MEMORY: `read_request` allocates its header and body buffers
// from `context.allocator` — the caller's per-request arena — so every
// view in a `Request` lives exactly as long as the request being served.
//
// LIMITS: the header block must fit HDR_CAP (8K); a body must declare
// Content-Length (411 otherwise) and fit MAX_BODY (413 otherwise). Bodies
// are read whole into memory — streaming uploads are a later slice.
//
// NOT handled (v0, documented): keep-alive (every response closes),
// chunked transfer, TLS (the deployment plan terminates TLS at a reverse
// proxy).
// =====================================================================
#import "modules/std.sx";
sock :: #import "modules/std/socket.sx";
HttpError :: error {
Socket,
Bind,
Listen,
}
// Why a request could not be read. `Closed` = the peer sent nothing
// (speculative preconnect or a dropped connection) — not worth a response.
// Every other tag maps to a 4xx the caller sends.
ReadErr :: error {
Closed, // no bytes arrived (idle preconnect / disconnect)
BadRequest, // request line unparseable, or the body never finished
HeadersTooLarge, // header block exceeds HDR_CAP
LengthRequired, // a method with a body arrived without Content-Length
BodyTooLarge, // declared Content-Length exceeds MAX_BODY
}
HDR_CAP :: 8192;
MAX_BODY :: 536870912; // 512 MiB — bodies are held in memory whole
// One parsed request. All fields are views into per-request arena memory
// owned by `read_request`.
Request :: struct {
method: string = "";
path: string = "";
headers: string = ""; // raw header block, between request line and the blank line
body: string = "";
}
// Open a listening socket on 0.0.0.0:<port>. SO_REUSEADDR so a restarted
// server can re-bind without waiting out TIME_WAIT.
listen_on :: (port: i64) -> (i32, !HttpError) {
fd := sock.socket(sock.AF_INET, sock.SOCK_STREAM, 0);
if fd < 0 { raise error.Socket; }
opt : i32 = 1;
sock.setsockopt(fd, sock.SOL_SOCKET, sock.SO_REUSEADDR, @opt, 4);
addr : sock.SockAddr = .{ sin_len = 16, sin_family = 2, sin_port = sock.htons(port) };
if sock.bind(fd, @addr, 16) < 0 { sock.close(fd); raise error.Bind; }
if sock.listen(fd, 16) < 0 { sock.close(fd); raise error.Listen; }
return fd;
}
SO_RCVTIMEO :i32: 0x1006; // macOS
// macOS struct timeval (padded to 16 for the setsockopt copy).
Timeval :: struct {
tv_sec: i64;
tv_usec: i32 = 0;
pad: i32 = 0;
}
// Bound blocking reads on `fd` to `ms` milliseconds. A sequential accept
// loop needs this: browsers open speculative preconnections that never
// send bytes, and an unbounded read on one of those wedges the whole
// server while real requests sit in the backlog. Every idle socket costs
// the full timeout serially, so it must stay well under human patience;
// a LAN client delivers its request bytes within a few ms of connecting.
// (Retired by PLAN-HTTPZ A1, when the loop goes readiness-based.)
set_read_timeout :: (fd: i32, ms: i64) {
tv : Timeval = .{ tv_sec = ms / 1000, tv_usec = xx ((ms % 1000) * 1000) };
sock.setsockopt(fd, sock.SOL_SOCKET, SO_RCVTIMEO, xx @tv, 16);
}
// Parse the request line `METHOD SP PATH SP HTTP/x.y` off the raw bytes.
// False when the bytes don't look like an HTTP request line.
parse_request :: (raw: string, req: *Request) -> bool {
i := 0;
while i < raw.len and raw[i] != 32 { i += 1; } // 32 = ' '
if i == 0 or i >= raw.len { return false; }
req.method = string.{ ptr = raw.ptr, len = i };
j := i + 1;
k := j;
while k < raw.len and raw[k] != 32 and raw[k] != 13 { k += 1; } // 13 = '\r'
if k == j or k >= raw.len { return false; }
if raw[k] != 32 { return false; } // no HTTP version after path
req.path = string.{ ptr = @raw[j], len = k - j };
return true;
}
// Case-insensitive ASCII equality for header names.
hname_eq :: (a: string, b: string) -> bool {
if a.len != b.len { return false; }
i := 0;
while i < a.len {
ca := a[i];
cb := b[i];
if ca >= 65 and ca <= 90 { ca += 32; } // 'A'..'Z' -> lower
if cb >= 65 and cb <= 90 { cb += 32; }
if ca != cb { return false; }
i += 1;
}
return true;
}
// The value of header `name` (case-insensitive) in a raw header block —
// leading spaces/tabs trimmed, null when absent.
header_value :: (headers: string, name: string) -> ?string {
i := 0;
while i < headers.len {
// the current line: [i, eol)
eol := i;
while eol < headers.len and headers[eol] != 13 { eol += 1; } // 13 = '\r'
// split at ':'
c := i;
while c < eol and headers[c] != 58 { c += 1; } // 58 = ':'
if c < eol {
nm := string.{ ptr = @headers[i], len = c - i };
if hname_eq(nm, name) {
v := c + 1;
while v < eol and (headers[v] == 32 or headers[v] == 9) { v += 1; }
return string.{ ptr = @headers[v], len = eol - v };
}
}
// skip "\r\n"
i = eol + 2;
}
return null;
}
// Non-negative decimal, or null (empty/garbage/overflow-length input).
parse_content_length :: (s: string) -> ?i64 {
if s.len == 0 or s.len > 12 { return null; }
v : i64 = 0;
i := 0;
while i < s.len {
c := s[i];
if c < 48 or c > 57 { return null; } // '0'..'9'
v = v * 10 + (c - 48);
i += 1;
}
return v;
}
// Find "\r\n\r\n" in buf[0..len); -1 when absent.
find_blank_line :: (buf: [*]u8, len: i64) -> i64 {
i : i64 = 0;
while i + 3 < len {
if buf[i] == 13 and buf[i+1] == 10 and buf[i+2] == 13 and buf[i+3] == 10 {
return i;
}
i += 1;
}
return -1;
}
// Read one full request off `client`: request line + headers (up to
// HDR_CAP), then — when Content-Length says so — the whole body. GETs
// carry no body; any request DECLARING a body gets it read regardless of
// method, so the router can 405 with the connection drained.
read_request :: (client: i32, req: *Request) -> !ReadErr {
hbuf : [*]u8 = xx context.allocator.alloc_bytes(HDR_CAP);
filled : i64 = 0;
hdr_end : i64 = -1;
while hdr_end < 0 {
if filled >= HDR_CAP { raise error.HeadersTooLarge; }
n := sock.read(client, @hbuf[filled], xx (HDR_CAP - filled));
if n <= 0 {
if filled == 0 { raise error.Closed; }
raise error.BadRequest;
}
filled += n;
hdr_end = find_blank_line(hbuf, filled);
}
head := string.{ ptr = hbuf, len = hdr_end };
if !parse_request(head, req) { raise error.BadRequest; }
// header block = after the request line, before the blank line
line_end := 0;
while line_end < head.len and head[line_end] != 13 { line_end += 1; }
hstart := line_end + 2;
if hstart < hdr_end {
req.headers = string.{ ptr = @hbuf[hstart], len = hdr_end - hstart };
} else {
req.headers = "";
}
// body: Content-Length governs; absent means none expected — except
// for POST/PUT, which must declare one (411) so an upload can never
// be silently truncated.
clq := header_value(req.headers, "content-length");
if clq == null {
if req.method == "POST" or req.method == "PUT" { raise error.LengthRequired; }
req.body = "";
return;
}
clv := parse_content_length(clq!);
if clv == null { raise error.BadRequest; }
body_len := clv!;
if body_len == 0 { req.body = ""; return; }
if body_len > MAX_BODY { raise error.BodyTooLarge; }
bbuf : [*]u8 = xx context.allocator.alloc_bytes(xx body_len);
have : i64 = 0;
// bytes that arrived in the header read belong to the body
spill := filled - (hdr_end + 4);
if spill > 0 {
take := if spill > body_len then body_len else spill;
memcpy(bbuf, @hbuf[hdr_end + 4], xx take);
have = take;
}
while have < body_len {
n := sock.read(client, @bbuf[have], xx (body_len - have));
if n <= 0 { raise error.BadRequest; } // peer quit mid-body
have += n;
}
req.body = string.{ ptr = bbuf, len = body_len };
return;
}
status_text :: (code: i64) -> string {
if code == 200 { return "OK"; }
if code == 400 { return "Bad Request"; }
if code == 401 { return "Unauthorized"; }
if code == 403 { return "Forbidden"; }
if code == 404 { return "Not Found"; }
if code == 405 { return "Method Not Allowed"; }
if code == 409 { return "Conflict"; }
if code == 411 { return "Length Required"; }
if code == 413 { return "Payload Too Large"; }
if code == 503 { return "Service Unavailable"; }
return "Internal Server Error";
}
// Write one complete response and leave the connection for the caller to
// close. `extra` is zero or more pre-formatted header lines, each ending
// in `\r\n` ("" for none).
respond :: (client: i32, code: i64, content_type: string, extra: string, body: string) {
h := concat("HTTP/1.1 ", concat(int_to_string(code), concat(" ", status_text(code))));
h = concat(h, "\r\n");
h = concat(h, concat("Content-Type: ", concat(content_type, "\r\n")));
h = concat(h, concat("Content-Length: ", concat(int_to_string(body.len), "\r\n")));
h = concat(h, "Connection: close\r\n");
h = concat(h, extra);
h = concat(h, "\r\n");
sock.write(client, h.ptr, xx h.len);
if body.len > 0 { sock.write(client, body.ptr, xx body.len); }
}

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")));