Skip to content

Commit cd69bc3

Browse files
committed
http2: always delay closing the connection after sending GOAWAY
Currently, we close the connection immediately after sending a GOAWAY frame if all outstanding responses have been completely sent. However, the client may have had requests in-flight at that time which have been queued in the kernel receive buffer. On both Windows and Linux, if the connection is close()'d when the receive buffer is not empty, the kernel sends RST. This has the effect of aborting both sides of the connection, meaning the client may not actually receive all responses that were sent before the GOAWAY. Instead, we should delay calling close() until after the receive buffer has been drained. We don't want to delay indefinitely, which means we need some kind of timeout. Ideally that timeout should be about 1 RTT + epsilon, under the assumption that the client will not send any more frames after receiving the GOAWAY. However, 1 RTT is difficult to measure. It turns out we were already using a 1 second delay in other cases, so we reuse that same delay here. Note that we do not call CloseWrite() to half-close the underlying TLS connection. This seems unnecessary -- GOAWAY is effectively a half-close at the HTTP/2 level. Updates golang/go#18701 (fixes after it's bundled into net/http) Change-Id: I4d68bada6369ba95e5db02afe6dfad0a393c0334 Reviewed-on: https://go-review.googlesource.com/71372 Run-TryBot: Tom Bergan <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]> Reviewed-by: Joe Tsai <[email protected]>
1 parent aabf507 commit cd69bc3

File tree

3 files changed

+26
-24
lines changed

3 files changed

+26
-24
lines changed

http2/server.go

+24-18
Original file line numberDiff line numberDiff line change
@@ -853,8 +853,13 @@ func (sc *serverConn) serve() {
853853
}
854854
}
855855

856-
if sc.inGoAway && sc.curOpenStreams() == 0 && !sc.needToSendGoAway && !sc.writingFrame {
857-
return
856+
// Start the shutdown timer after sending a GOAWAY. When sending GOAWAY
857+
// with no error code (graceful shutdown), don't start the timer until
858+
// all open streams have been completed.
859+
sentGoAway := sc.inGoAway && !sc.needToSendGoAway && !sc.writingFrame
860+
gracefulShutdownComplete := sc.goAwayCode == ErrCodeNo && sc.curOpenStreams() == 0
861+
if sentGoAway && sc.shutdownTimer == nil && (sc.goAwayCode != ErrCodeNo || gracefulShutdownComplete) {
862+
sc.shutDownIn(goAwayTimeout)
858863
}
859864
}
860865
}
@@ -1218,30 +1223,31 @@ func (sc *serverConn) startGracefulShutdown() {
12181223
sc.shutdownOnce.Do(func() { sc.sendServeMsg(gracefulShutdownMsg) })
12191224
}
12201225

1226+
// After sending GOAWAY, the connection will close after goAwayTimeout.
1227+
// If we close the connection immediately after sending GOAWAY, there may
1228+
// be unsent data in our kernel receive buffer, which will cause the kernel
1229+
// to send a TCP RST on close() instead of a FIN. This RST will abort the
1230+
// connection immediately, whether or not the client had received the GOAWAY.
1231+
//
1232+
// Ideally we should delay for at least 1 RTT + epsilon so the client has
1233+
// a chance to read the GOAWAY and stop sending messages. Measuring RTT
1234+
// is hard, so we approximate with 1 second. See golang.org/issue/18701.
1235+
//
1236+
// This is a var so it can be shorter in tests, where all requests uses the
1237+
// loopback interface making the expected RTT very small.
1238+
//
1239+
// TODO: configurable?
1240+
var goAwayTimeout = 1 * time.Second
1241+
12211242
func (sc *serverConn) startGracefulShutdownInternal() {
1222-
sc.goAwayIn(ErrCodeNo, 0)
1243+
sc.goAway(ErrCodeNo)
12231244
}
12241245

12251246
func (sc *serverConn) goAway(code ErrCode) {
1226-
sc.serveG.check()
1227-
var forceCloseIn time.Duration
1228-
if code != ErrCodeNo {
1229-
forceCloseIn = 250 * time.Millisecond
1230-
} else {
1231-
// TODO: configurable
1232-
forceCloseIn = 1 * time.Second
1233-
}
1234-
sc.goAwayIn(code, forceCloseIn)
1235-
}
1236-
1237-
func (sc *serverConn) goAwayIn(code ErrCode, forceCloseIn time.Duration) {
12381247
sc.serveG.check()
12391248
if sc.inGoAway {
12401249
return
12411250
}
1242-
if forceCloseIn != 0 {
1243-
sc.shutDownIn(forceCloseIn)
1244-
}
12451251
sc.inGoAway = true
12461252
sc.needToSendGoAway = true
12471253
sc.goAwayCode = code

http2/server_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type serverTester struct {
6868

6969
func init() {
7070
testHookOnPanicMu = new(sync.Mutex)
71+
goAwayTimeout = 25 * time.Millisecond
7172
}
7273

7374
func resetHooks() {

http2/write.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"log"
1111
"net/http"
1212
"net/url"
13-
"time"
1413

1514
"golang.org/x/net/http2/hpack"
1615
"golang.org/x/net/lex/httplex"
@@ -90,11 +89,7 @@ type writeGoAway struct {
9089

9190
func (p *writeGoAway) writeFrame(ctx writeContext) error {
9291
err := ctx.Framer().WriteGoAway(p.maxStreamID, p.code, nil)
93-
if p.code != 0 {
94-
ctx.Flush() // ignore error: we're hanging up on them anyway
95-
time.Sleep(50 * time.Millisecond)
96-
ctx.CloseConn()
97-
}
92+
ctx.Flush() // ignore error: we're hanging up on them anyway
9893
return err
9994
}
10095

0 commit comments

Comments
 (0)