Skip to content

perf(durable-streams-rust): serve caught-up long-polls from the epoll reactor#4667

Open
balegas wants to merge 1 commit into
mainfrom
long-poll-reactor
Open

perf(durable-streams-rust): serve caught-up long-polls from the epoll reactor#4667
balegas wants to merge 1 commit into
mainfrom
long-poll-reactor

Conversation

@balegas

@balegas balegas commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Generalizes the Linux epoll reactor (renamed sse_reactorreactor) to serve two reader modes from one shared slab/epoll/flush/wake core. A caught-up long-poll on a reactor-eligible stream now hands its socket to the reactor instead of parking the whole connection-task future for the up-to-30s wait — the same per-subscriber memory win the SSE reactor already gives live tails.

  • The reactor writes the single long-poll response on data / close / timeout, then hands the socket back for keep-alive so polling clients reuse the connection (no per-poll churn). When the next request is another same-stream long-poll, it's served in place without leaving the reactor.
  • Kind::{Sse, LongPoll} splits only what differs (what to produce on a wake, what to do when finished); slab, epoll arm/flush/backpressure, cross-thread wake and lifetime sweep are shared.
  • Long-poll handlers were refactored into sync, reusable builders (long_poll_data_resp, long_poll_close, long_poll_timeout) so the inline path and the reactor emit byte-identical responses.
  • Linux only; non-Linux and cold/forked/tiered streams keep the proven inline wait. SSE behaviour is unchanged.

Socket-closure (Connection: close) check

The base SSE PR's final fix marks SSE responses Connection: close because SSE unilaterally closes the socket (close-instead-of-FIN → RST → discarded in-flight response on a pooling client). I verified the long-poll reactor does not have this class of bug, by construction:

  • The advertised Connection header is tied to the same keep_alive flag that decides close-vs-handback (write_head emits connection: close iff !keep_alive; finish keeps the socket iff keep_alive). The disposition can never disagree with what was advertised.
  • Every close() of a keep-alive socket happens with a drained recv buffer (the in-place reader reads to EAGAIN before deciding; parked closes carry no in-flight body), so it's a clean FIN, never an RST.

The reactor's SSE path inherits the Connection: close fix automatically (its head is built from handle_sse's headers).

Testing

  • Linux cross-compile (x86_64-unknown-linux-gnu) and native builds pass.
  • Reactor unit tests cover long-poll serialization (200 data wrap, 204 close → connection: close, 204 timeout → keep-alive).
  • Conformance suite covers both SSE and long-poll against the containerized Linux server.

🤖 Generated with Claude Code

… reactor

Generalize the SSE reactor (renamed sse_reactor -> reactor) to serve two reader
modes from one slab/epoll/flush/wake core. A caught-up long-poll on a reactor-
eligible stream now hands its socket to the reactor instead of parking the whole
connection-task future for the wait; the reactor writes the single response on
data/close/timeout and, for a keep-alive client, hands the socket back to the
async connection loop so the client's next request is read and dispatched there
(re-registering with the reactor if it is another caught-up poll). Reusing the
existing async keep-alive path keeps the reactor free of a second HTTP request
reader. Linux only; non-Linux and cold/forked/tiered streams keep the inline
wait. SSE behaviour is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@balegas balegas force-pushed the long-poll-reactor branch from 9647ac4 to ebd8016 Compare June 30, 2026 23:01
@balegas

balegas commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Simplified: dropped the in-reactor keep-alive request reader (design 1)

Per review, removed the ~200-line in-reactor HTTP request reader. After writing a keep-alive long-poll response the reactor now hands the socket back to the async conn_loop, which reads and dispatches the client's next request (re-registering with the reactor if it's another caught-up poll). This reuses the existing, proven async keep-alive path instead of maintaining a second request reader in the reactor.

Net effect:

  • reactor.rs 1272 → 1073 lines (−199); removed read_request, enter_reading_request, arm/disarm_epollin, the LpPhase enum, the EPOLLIN path, and parse_long_poll_continuation (handlers).
  • Diff vs main shrank from +1494/−709 to +661/−129.
  • The only thing left mode-specific to long-poll is the response build (longpoll_produce/tick/finalize/serialize) + handback; slab/epoll/flush/backpressure/wake/lifetime-sweep are fully shared with SSE.

Validation (Linux):

  • 94 unit tests pass; native build + clippy (--all-targets) clean.
  • Conformance suite: 11 consecutive full runs, 326 passed each, 0 failures (the closure/connection-reuse race historically reproduced ~1/19), including the long-poll "wait for new data" and SSE stream-closure tests.

balegas added a commit that referenced this pull request Jul 3, 2026
…neralized reactor

Merges origin/long-poll-reactor (serve caught-up long-polls from the epoll
reactor, sse_reactor -> reactor rename) and adapts it to this branch:

- Port the per-stream wake-coalescing latch (StreamSubs::wake_pending) into
  the generalized reactor: wake_stream's queued-wake skip, the drain loop's
  clear-before-produce (now covering both Kind::Sse and Kind::LongPoll —
  longpoll_produce reads durable_tail after the clear, preserving the
  clear-then-read invariant), and the register-resets-latch rule. Latch and
  registration stay serialized under the reactor_subs mutex, so a long-poll
  registering against an in-flight coalesced wake cannot be stranded.
- Resolve handlers.rs against the removed splice append path (the incoming
  branch predated its deletion) and drop the now-unused AtomicBool import in
  engine_raw.rs.
- Fold the long-poll changeset into the branch's single consolidated
  changeset; document the reactor-served long-poll wait and wake coalescing
  in ARCHITECTURE.md's read-path section.

Validation: cargo test --release 100 passed / 0 failed (2 ignored,
Linux-only); cargo clippy --all-targets -D warnings clean for
x86_64-unknown-linux-gnu (compiles the reactor + its unit tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant