Loading src/http/HttpProxyServer.cpp +9 −17 Changes for src/http/HttpProxyServer.cpp: 9 added lines, 17 removed lines. Original line number Diff line number Diff line Loading @@ -117,24 +117,16 @@ HttpProxyController::HttpProxyController(std::vector<netplus::socket *> serverSo http::UpstreamConnectionPool &connectionPool, tls::AcmeChallengeStore *acmeChallenges, int idleTimeoutSeconds) // h2OffloadThreads/h1OffloadThreads non-zero (see shouldOffloadH2Dispatch/ // shouldOffloadH1Dispatch overrides below): every RequestEvent here does a real backend // round-trip (HttpClient::Get/Post/etc., possibly against a slow/overloaded upstream) and // would otherwise run synchronously on this listener's fixed, hardware_concurrency()-sized // worker pool -- shared across every domain/route on the same listener, not just the one // being proxied. Left at 0 (fully synchronous) this meant a handful of slow backend // requests (observed live: a crawler hammering a proxied git server's /users/sign_in and // deep commit/blame history) could tie up every worker thread at once, making completely // unrelated traffic on the same listener (e.g. a WebSocket upgrade to a different backend) // appear to hang -- see [[project_proxyplus_tls_hang_recurrence_20260816]]. H2 streams // always went through _h2DispatchPool; H1 requests (including a WebSocket upgrade's // blocking connect+handshake read) had no such option until _dispatchH1Request/ // detachConnection+reattachConnection existed to make it safe for H1's one-request-per- // connection model. Both pools sized like h3StreamPool (libhttppp/src/httpd.cpp) and // quicDispatchPool() (libnetplus/src/quic.cpp): oversubscribed 2x hardware_concurrency() // since this work is dominated by waiting on the network, not CPU. // Keep H2 dispatch synchronous for now. Enabling libhttppp's H2 offload pool makes an // otherwise healthy proxied response remain queued after RequestEvent returns: the client // completes TLS/ALPN and sends its stream, but receives no response frames. HTTP/1.1 // continues to work against the same route/upstream, which makes this especially damaging // for browsers (they normally negotiate h2). H1 offloading is independent and remains // enabled so slow backend round-trips and WebSocket handshakes do not occupy the shared // listener workers. Re-enable H2 offloading only after its response handoff/flush path has // an integration test that observes the response on the client socket. : libhttppp::HttpEvent(std::move(serverSockets), /*timeout=*/1000, /*h2OffloadThreads=*/(std::max<size_t>)(4, std::thread::hardware_concurrency() * 2), /*h2OffloadThreads=*/0, idleTimeoutSeconds, /*h1OffloadThreads=*/(std::max<size_t>)(4, std::thread::hardware_concurrency() * 2)) , _listenerLabel(std::move(listenerLabel)) Loading src/http/HttpProxyServer.h +3 −13 Changes for src/http/HttpProxyServer.h: 3 added lines, 13 removed lines. Original line number Diff line number Diff line Loading @@ -42,19 +42,9 @@ public: void RequestEvent(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args) override; // Always offload: RequestEvent's real work here (see the constructor's h2OffloadThreads // comment in HttpProxyServer.cpp) is a backend network round-trip for every route this // class ever forwards, so there's no synchronous-vs-offload split to make per-route -- // unlike libhttppp::HttpEvent's own base default (which assumes most callers have a mix // of cheap and slow routes), a reverse proxy doesn't have a "cheap" H2 stream to keep off // the offload pool. bool shouldOffloadH2Dispatch(libhttppp::HttpRequest &tempreq, uint32_t streamId) const override { return true; } // Same rationale as shouldOffloadH2Dispatch() above, for HTTP/1.x: every route here does a // real backend round-trip, and a WebSocket upgrade's initial connect+handshake read (see // WebSocketProxy.cpp's handleWebSocketUpgrade) is itself dispatched through this same H1 // Every route here does a real backend round-trip, and a WebSocket upgrade's initial // connect+handshake read (see WebSocketProxy.cpp's handleWebSocketUpgrade) is itself // dispatched through this same H1 // RequestEvent() path -- without this, that blocking work ran directly on one of the // process's few shared epoll/kqueue worker threads, the same class of thread-starvation // bug tracked in [[project_proxyplus_tls_hang_recurrence_20260816]] (a burst of slow H1/H2 Loading Loading
src/http/HttpProxyServer.cpp +9 −17 Changes for src/http/HttpProxyServer.cpp: 9 added lines, 17 removed lines. Original line number Diff line number Diff line Loading @@ -117,24 +117,16 @@ HttpProxyController::HttpProxyController(std::vector<netplus::socket *> serverSo http::UpstreamConnectionPool &connectionPool, tls::AcmeChallengeStore *acmeChallenges, int idleTimeoutSeconds) // h2OffloadThreads/h1OffloadThreads non-zero (see shouldOffloadH2Dispatch/ // shouldOffloadH1Dispatch overrides below): every RequestEvent here does a real backend // round-trip (HttpClient::Get/Post/etc., possibly against a slow/overloaded upstream) and // would otherwise run synchronously on this listener's fixed, hardware_concurrency()-sized // worker pool -- shared across every domain/route on the same listener, not just the one // being proxied. Left at 0 (fully synchronous) this meant a handful of slow backend // requests (observed live: a crawler hammering a proxied git server's /users/sign_in and // deep commit/blame history) could tie up every worker thread at once, making completely // unrelated traffic on the same listener (e.g. a WebSocket upgrade to a different backend) // appear to hang -- see [[project_proxyplus_tls_hang_recurrence_20260816]]. H2 streams // always went through _h2DispatchPool; H1 requests (including a WebSocket upgrade's // blocking connect+handshake read) had no such option until _dispatchH1Request/ // detachConnection+reattachConnection existed to make it safe for H1's one-request-per- // connection model. Both pools sized like h3StreamPool (libhttppp/src/httpd.cpp) and // quicDispatchPool() (libnetplus/src/quic.cpp): oversubscribed 2x hardware_concurrency() // since this work is dominated by waiting on the network, not CPU. // Keep H2 dispatch synchronous for now. Enabling libhttppp's H2 offload pool makes an // otherwise healthy proxied response remain queued after RequestEvent returns: the client // completes TLS/ALPN and sends its stream, but receives no response frames. HTTP/1.1 // continues to work against the same route/upstream, which makes this especially damaging // for browsers (they normally negotiate h2). H1 offloading is independent and remains // enabled so slow backend round-trips and WebSocket handshakes do not occupy the shared // listener workers. Re-enable H2 offloading only after its response handoff/flush path has // an integration test that observes the response on the client socket. : libhttppp::HttpEvent(std::move(serverSockets), /*timeout=*/1000, /*h2OffloadThreads=*/(std::max<size_t>)(4, std::thread::hardware_concurrency() * 2), /*h2OffloadThreads=*/0, idleTimeoutSeconds, /*h1OffloadThreads=*/(std::max<size_t>)(4, std::thread::hardware_concurrency() * 2)) , _listenerLabel(std::move(listenerLabel)) Loading
src/http/HttpProxyServer.h +3 −13 Changes for src/http/HttpProxyServer.h: 3 added lines, 13 removed lines. Original line number Diff line number Diff line Loading @@ -42,19 +42,9 @@ public: void RequestEvent(libhttppp::HttpRequest &curreq, const int tid, ULONG_PTR args) override; // Always offload: RequestEvent's real work here (see the constructor's h2OffloadThreads // comment in HttpProxyServer.cpp) is a backend network round-trip for every route this // class ever forwards, so there's no synchronous-vs-offload split to make per-route -- // unlike libhttppp::HttpEvent's own base default (which assumes most callers have a mix // of cheap and slow routes), a reverse proxy doesn't have a "cheap" H2 stream to keep off // the offload pool. bool shouldOffloadH2Dispatch(libhttppp::HttpRequest &tempreq, uint32_t streamId) const override { return true; } // Same rationale as shouldOffloadH2Dispatch() above, for HTTP/1.x: every route here does a // real backend round-trip, and a WebSocket upgrade's initial connect+handshake read (see // WebSocketProxy.cpp's handleWebSocketUpgrade) is itself dispatched through this same H1 // Every route here does a real backend round-trip, and a WebSocket upgrade's initial // connect+handshake read (see WebSocketProxy.cpp's handleWebSocketUpgrade) is itself // dispatched through this same H1 // RequestEvent() path -- without this, that blocking work ran directly on one of the // process's few shared epoll/kqueue worker threads, the same class of thread-starvation // bug tracked in [[project_proxyplus_tls_hang_recurrence_20260816]] (a burst of slow H1/H2 Loading