Add second cork buffer to uWebSockets for nested async handlers
Merged
JA
7mo ago+540+540+012345678901234567890123456789-161-161-012345678901234567890123456789
What
Replaces uWebSockets' single shared cork buffer with two independent cork slots.
Why
When an async HTTP handler awaits and later resumes (e.g. another request resolves its promise), the resumed request's writeHead() + end() couldn't get the cork buffer because the newer in-flight request was holding it. This forced the resumed request down the uncorked path — one write() syscall per call instead of batching them. On the bench/snippets/http-hello.node.mjs interleaved-await pattern this dropped throughput from ~190k req/s to ~22k req/s.
How
- Two independent slots: each holds
{buffer, socket, offset, ssl}.cork()grabs whichever slot is free;uncork()releases yours. No ordering constraints. - Empty-slot stealing: if both slots are claimed but one has
offset==0(corked but not yet written), it's freely stolen — no flush needed. - LRU eviction: when both slots hold actual data (rare, depth 3+), force-uncork the least-recently-touched one. Tracked via a single bit.
- Entry-point re-cork:
NodeHTTP writeHeadandHttpResponse::cork(handler)callcork()before each write sequence, so if your slot was stolen while you were awaiting, you transparently get a fresh one. - UAF protection:
uncorkWithoutSending()(called from all close/destroy paths) now clears the socket from any slot viaunborrowCorkSlot(), preventing the drain loop from dereferencing a freed pointer. - Bounded drain:
preCbanduws_res_clear_corked_socketiterate at most twice (one per slot).
Memory cost: one extra 16KB buffer per event loop.
Tests
test/js/node/http/node-http-nested-cork.test.ts— 20 concurrent interleaved-await requests for both node:http and Bun.serve, asserts all responses are correct with no cross-socket bleed.serve.test.ts(189 tests),node-http.test.ts(78 tests),websocket-server.test.tsall pass.