go: net/http, x/net/http2: http server shutdown doesn't gracefully shut down HTTP2 connections

What version of Go are you using (go version)?

$ go version
go version go1.13.7 darwin/amd64

Does this issue reproduce with the latest release?

Yes, tested on go version go1.14.4 darwin/amd64

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/lszaszki/Library/Caches/go-build"
GOENV="/Users/lszaszki/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/lszaszki/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/4x/d8vbcg5s6lx229_q7l5wbw0h0000gn/T/go-build581125651=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

I wrote a simple unit test that sets up an HTTP2 connection between a client and the server, sends a few requests and shuts down the backend server as soon as the last request arrives at the server. Since each request waits for 60 seconds in the handler I consider these requests to be active.

I analyzed net/http package and here’s what I have found out. On calling http.server.Shutdown() the server will be closed immediately because the connection is in StateNew (not in StateActive). In general net/http package manages connection states and provides a hook (ConnState) for reporting connections state. For HTTP 1.x it will transition the connection to StateActive as soon as it reads any byte on the wire. For HTTP 2.x h2_bundle.go will transition the connection to StateActive once there is more than one active stream. But the issue here is that ConnState hook is not wired. At least I haven’t found the wiring. So the server doesn’t know the current state of the connection.

I changed net/http so that it provides a default ConnState method. It is used by the HTTP2 implementation to report back the connection state. With this modification, my test takes ~60 seconds. The server waits until all requests complete before shutting down and all requests complete with success (HTTP 200).

What did you expect to see?

According to the documentation, the Shutdown method must wait indefinitely for active connections to return to the idle state and only then shut down the server. In that case, I would expect all requests to complete with success (HTTP 200) and to test take at least 60 seconds because each request simulates work by sleeping for that duration.

What did you see instead?

My test ended after ~6 seconds, all requests failed with Get "https://localhost:52066": http2: server sent GOAWAY and closed the connection; LastStreamID=49, ErrCode=NO_ERROR, debug="".

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 32 (11 by maintainers)

Most upvoted comments

You are correct. The connection state is not being managed properly. I haven’t quite figured out a good way to fix this yet.

SGTM, thank you @networkimprov for the tag and the triage gardening, I’ll take a look later today.

On Thu, Sep 3, 2020 at 2:32 PM Liam notifications@github.com wrote:

The backport issues were closed. Once fixed, new backport issues should be manually posted for 1.15 & 1.14.

cc @odeke-em https://github.com/odeke-em in case he could review the CL.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/39776#issuecomment-686775217, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABFL3V533GNLYH3MUMGUQKLSEADOFANCNFSM4OFNVDXQ .

They are never marked idle. They are marked closed. Do not confuse the fact that http/2 marks streams as idle and active. This detail flows to the ConnState hook but never to the http/1 side.