From handler goroutines, don't assume serve loop goroutine is still active.
From running the production demo site (http2.golang.org), I noticed
three places where the handler goroutines would be forever blocked on
a channel operation because the client had disconnected, thus leaking
the goroutine.
diff --git a/server.go b/server.go
index cb4eb63..c33eb3a 100644
--- a/server.go
+++ b/server.go
@@ -42,6 +42,9 @@
// be in-flight and then the frame scheduler in the serve goroutine
// will be responsible for splitting things.
+// TODO: send PING frames to idle clients and disconnect them if no
+// reply
+
// Server is an HTTP/2 server.
type Server struct {
// MaxStreams optionally ...
@@ -352,9 +355,8 @@
var errClientDisconnected = errors.New("client disconnected")
-func (sc *serverConn) stopServing() {
+func (sc *serverConn) closeAllStreamsOnConnClose() {
sc.serveG.check()
- close(sc.writeFrameCh) // stop the writeFrames loop
for _, st := range sc.streams {
sc.closeStream(st, errClientDisconnected)
}
@@ -363,7 +365,9 @@
func (sc *serverConn) serve() {
sc.serveG.check()
defer sc.conn.Close()
- defer sc.stopServing()
+ defer sc.closeAllStreamsOnConnClose()
+ defer close(sc.doneServing) // unblocks handlers trying to send
+ defer close(sc.writeFrameCh) // stop the writeFrames loop
sc.vlogf("HTTP/2 connection from %v on %p", sc.conn.RemoteAddr(), sc.hs)
@@ -444,8 +448,12 @@
// should be called from non-serve() goroutines, otherwise the ends may deadlock
// the serve loop. (it's only buffered a little bit).
func (sc *serverConn) writeFrame(wm frameWriteMsg) {
- sc.serveG.checkNotOn() // note the "NOT"
- sc.wantWriteFrameCh <- wm
+ sc.serveG.checkNotOn() // NOT
+ select {
+ case sc.wantWriteFrameCh <- wm:
+ case <-sc.doneServing:
+ // Client has closed their connection to the server.
+ }
}
func (sc *serverConn) enqueueFrameWrite(wm frameWriteMsg) {
@@ -1113,7 +1121,13 @@
endStream: req.endStream,
})
if errc != nil {
- <-errc
+ select {
+ case <-errc:
+ // Ignore. Just for synchronization.
+ // Any error will be handled in the writing goroutine.
+ case <-sc.doneServing:
+ // Client has closed the connection.
+ }
}
}
@@ -1367,7 +1381,13 @@
done: rws.chunkWrittenCh,
v: rws, // writeDataInLoop uses only rws.curChunk and rws.curChunkIsFinal
})
- err = <-rws.chunkWrittenCh // block until it's written
+ // Block until it's written, or if the client disconnects.
+ select {
+ case err = <-rws.chunkWrittenCh:
+ case <-rws.stream.conn.doneServing:
+ // Client disconnected.
+ err = errClientDisconnected
+ }
if err != nil {
break
}