P4.1-001: 2s read timeout on accepted sockets (idle preconnect wedged the loop)
A browser speculative preconnection sends no bytes; the sequential accept loop blocked in read() on it forever while real requests sat in the backlog — LAN clients saw a dead server while curl (connect+send in one shot) worked. SO_RCVTIMEO frees the loop. Regression case pinned in tests/server_http.sx (fails 000 pre-fix, 200 post-fix).
This commit is contained in:
@@ -290,6 +290,7 @@ run_server :: (store_dir: string, port: s64) -> !ServeErr {
|
||||
while true {
|
||||
client := sock.accept(fd, null, null);
|
||||
if client < 0 { continue; }
|
||||
http.set_read_timeout(client, 2);
|
||||
|
||||
gpa := GPA.init();
|
||||
arena := Arena.init(xx gpa, 65536);
|
||||
|
||||
@@ -46,6 +46,24 @@ listen_on :: (port: s64) -> (s32, !HttpError) {
|
||||
return fd;
|
||||
}
|
||||
|
||||
SO_RCVTIMEO :s32: 0x1006; // macOS
|
||||
|
||||
// macOS struct timeval (padded to 16 for the setsockopt copy).
|
||||
Timeval :: struct {
|
||||
tv_sec: s64;
|
||||
tv_usec: s32 = 0;
|
||||
pad: s32 = 0;
|
||||
}
|
||||
|
||||
// Bound blocking reads on `fd` to `secs` seconds. 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.
|
||||
set_read_timeout :: (fd: s32, secs: s64) {
|
||||
tv : Timeval = .{ tv_sec = secs };
|
||||
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 {
|
||||
|
||||
@@ -149,6 +149,18 @@ main :: () -> s32 {
|
||||
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) &'");
|
||||
process.run("sleep 0.3");
|
||||
wc := process.run(concat(concat("curl -s -m 5 -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");
|
||||
|
||||
// ── freshness: publish B while the server runs ────────────────────
|
||||
rb := process.run(publish_cmd(path_join(MDIR, "b.json")));
|
||||
process.assert(rb != null and rb!.exit_code == 0, "publish B must exit 0");
|
||||
|
||||
Reference in New Issue
Block a user