Skip to content

net/http: cherry-pick http2 serveConn goroutine removal PRs - #187

Merged
bradfitz merged 2 commits into
tailscale.go1.27from
bradfitz/http2_serveconn
Sep 15, 2026
Merged

bradfitz merged 2 commits into
tailscale.go1.27from
bradfitz/http2_serveconn

Conversation

@bradfitz

@bradfitz bradfitz commented Sep 15, 2026 •

Copy link
Copy Markdown
Member
  • [tailscale-cherrypick] net/http/internal/http2: let the server's serve goroutine exit when idle
  • [tailscale-cherrypick] net/http/internal/http2: do some follow-ups to serve parking change

…e goroutine exit when idle

Implement the TODO from 2014 at the top of server.go: a server
connection with nothing to do used to pin a goroutine blocked in the
serve loop (reached through net/http's conn.serve and ServeConn) in
addition to the readFrames goroutine; now it pins only readFrames.

When the serve loop has nothing runnable (nothing being written,
unflushed, or poppable on the write side, and no shutdown in
progress), the serve goroutine parks: it simply exits. Every sender
into the serve loop's channels first calls beginServeSend, which
counts in-flight sends and revives a parked loop on a new goroutine,
so a frame from the peer, a handler write or body read, a timer
firing, or a graceful shutdown request wakes the connection back up.

Open streams and running handlers don't prevent parking, since the
handler-side paths into the serve loop (response writes, body reads,
stream deadline timers, pushes) all go through beginServeSend too.
This helps SSE/long-poll-style servers: a connection whose handler is
parked mid-long-poll for hours pins only the readFrames and handler
goroutines, and the handler's next Write or Flush revives the loop.
With open streams, the write scheduler can still hold DATA frames
blocked on flow control; that's fine to park on, because only an
incoming WINDOW_UPDATE or SETTINGS frame can unblock them, and
incoming frames revive the loop.

The serve loop's teardown became explicit rather than deferred, and
it takes over the cleanup that ServeConn's callers used to do after
it returned (closing the connection, unregistering it, and running
net/http's StateClosed hook), signaled through the new
ServeConnOpts.OnClose hook. Callers that don't set OnClose keep the
old blocking ServeConn semantics and never park.

Parking happens between the frames of an active request, so the
park/resume cycle runs on hot paths. Spawning the resume goroutine
with a pre-allocated method-value closure keeps it allocation-free:
BenchmarkClientServer/h2 reports the same 68 allocs/op as before,
with ns/op unchanged within noise.

As a temporary safety measure, the GODEBUG=http2serveparking=0
setting disables parking, restoring the old behavior of keeping a
goroutine parked for the lifetime of each connection. The setting is
undocumented (#-prefixed), so it is not listed in doc/godebug.md or
runtime/metrics.

This is the server-side counterpart of CL 810780, which let the
Transport's request-write goroutine exit early for the same reason:
for servers with many mostly-idle connections, a parked goroutine and
its stack per connection add up.

Cherry-pick notes: the only conflict was in the serverConn struct
field list, where this tree still has the srv field that upstream
had removed and this change re-adds; the upstream field list was
taken. The doc/next release note was dropped, since the 1.27 release
branch has no doc/next directory.

Updates golang#80735
Updates golang#81524
Updates #174

Change-Id: I4a9fd4e2ce7a7a4e0f84179e54a9cc97d1a62ad5
Reviewed-on: https://go-review.googlesource.com/c/go/+/831084
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
(cherry picked from commit 5c51011)
… serve parking change

Fix a rebase comment mishap: the doc comment describing canPark was attached
to the http2serveparking var, leaving canPark undocumented.

Skip the parkMu accounting in beginServeSend and endServeSend on
connections that can never park (no OnClose hook, or parking disabled
with GODEBUG=http2serveparking=0), removing a mutex round trip per
frame on both the sender and receiver side.

Add a test that a response blocked on the peer's flow control window
parks the serve goroutine with its DATA queued in the write scheduler,
and that a WINDOW_UPDATE revives it.

Updates golang#80735
Updates golang#81524
Updates #174

Change-Id: Ic1a3f7b9d2e5a4c8f6b0d7a9e3c5f2b8d1a6e4c7
Reviewed-on: https://go-review.googlesource.com/c/go/+/832504
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
(cherry picked from commit 0ac31b6)
@bradfitz
bradfitz merged commit 5be7ee4 into tailscale.go1.27 Sep 15, 2026
5 checks passed
@bradfitz
bradfitz deleted the bradfitz/http2_serveconn branch September 15, 2026 03:55
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