Add second cork buffer to uWebSockets for nested async handlers

Merged
JA
7mo ago
+540-161

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 writeHead and HttpResponse::cork(handler) call cork() 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 via unborrowCorkSlot(), preventing the drain loop from dereferencing a freed pointer.
  • Bounded drain: preCb and uws_res_clear_corked_socket iterate 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.ts all pass.