Skip to content

Commit 4790dc7

Browse files
committedSep 25, 2024·
http2: add support for server-originated pings
Add configurable support for health-checking idle connections accepted by the HTTP/2 server, following the same configuration as the Transport. Fixes golang/go#67812 Change-Id: Ia4014e691546b2c29db8dad3af5f39966d0ceb93 Reviewed-on: https://go-review.googlesource.com/c/net/+/601497 Reviewed-by: Carlos Amedee <carlos@golang.org> Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent 541dbe5 commit 4790dc7

File tree

3 files changed

+113
-0
lines changed

3 files changed

+113
-0
lines changed
 

‎http2/server.go

+60
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"bufio"
3030
"bytes"
3131
"context"
32+
"crypto/rand"
3233
"crypto/tls"
3334
"errors"
3435
"fmt"
@@ -127,6 +128,16 @@ type Server struct {
127128
// If zero or negative, there is no timeout.
128129
IdleTimeout time.Duration
129130

131+
// ReadIdleTimeout is the timeout after which a health check using a ping
132+
// frame will be carried out if no frame is received on the connection.
133+
// If zero, no health check is performed.
134+
ReadIdleTimeout time.Duration
135+
136+
// PingTimeout is the timeout after which the connection will be closed
137+
// if a response to a ping is not received.
138+
// If zero, a default of 15 seconds is used.
139+
PingTimeout time.Duration
140+
130141
// WriteByteTimeout is the timeout after which a connection will be
131142
// closed if no data can be written to it. The timeout begins when data is
132143
// available to write, and is extended whenever any bytes are written.
@@ -644,9 +655,12 @@ type serverConn struct {
644655
inGoAway bool // we've started to or sent GOAWAY
645656
inFrameScheduleLoop bool // whether we're in the scheduleFrameWrite loop
646657
needToSendGoAway bool // we need to schedule a GOAWAY frame write
658+
pingSent bool
659+
sentPingData [8]byte
647660
goAwayCode ErrCode
648661
shutdownTimer timer // nil until used
649662
idleTimer timer // nil if unused
663+
readIdleTimer timer // nil if unused
650664

651665
// Owned by the writeFrameAsync goroutine:
652666
headerWriteBuf bytes.Buffer
@@ -974,11 +988,17 @@ func (sc *serverConn) serve() {
974988
defer sc.idleTimer.Stop()
975989
}
976990

991+
if sc.srv.ReadIdleTimeout > 0 {
992+
sc.readIdleTimer = sc.srv.afterFunc(sc.srv.ReadIdleTimeout, sc.onReadIdleTimer)
993+
defer sc.readIdleTimer.Stop()
994+
}
995+
977996
go sc.readFrames() // closed by defer sc.conn.Close above
978997

979998
settingsTimer := sc.srv.afterFunc(firstSettingsTimeout, sc.onSettingsTimer)
980999
defer settingsTimer.Stop()
9811000

1001+
lastFrameTime := sc.srv.now()
9821002
loopNum := 0
9831003
for {
9841004
loopNum++
@@ -992,6 +1012,7 @@ func (sc *serverConn) serve() {
9921012
case res := <-sc.wroteFrameCh:
9931013
sc.wroteFrame(res)
9941014
case res := <-sc.readFrameCh:
1015+
lastFrameTime = sc.srv.now()
9951016
// Process any written frames before reading new frames from the client since a
9961017
// written frame could have triggered a new stream to be started.
9971018
if sc.writingFrameAsync {
@@ -1023,6 +1044,8 @@ func (sc *serverConn) serve() {
10231044
case idleTimerMsg:
10241045
sc.vlogf("connection is idle")
10251046
sc.goAway(ErrCodeNo)
1047+
case readIdleTimerMsg:
1048+
sc.handlePingTimer(lastFrameTime)
10261049
case shutdownTimerMsg:
10271050
sc.vlogf("GOAWAY close timer fired; closing conn from %v", sc.conn.RemoteAddr())
10281051
return
@@ -1061,19 +1084,51 @@ func (sc *serverConn) serve() {
10611084
}
10621085
}
10631086

1087+
func (sc *serverConn) handlePingTimer(lastFrameReadTime time.Time) {
1088+
if sc.pingSent {
1089+
sc.vlogf("timeout waiting for PING response")
1090+
sc.conn.Close()
1091+
return
1092+
}
1093+
1094+
pingAt := lastFrameReadTime.Add(sc.srv.ReadIdleTimeout)
1095+
now := sc.srv.now()
1096+
if pingAt.After(now) {
1097+
// We received frames since arming the ping timer.
1098+
// Reset it for the next possible timeout.
1099+
sc.readIdleTimer.Reset(pingAt.Sub(now))
1100+
return
1101+
}
1102+
1103+
sc.pingSent = true
1104+
// Ignore crypto/rand.Read errors: It generally can't fail, and worse case if it does
1105+
// is we send a PING frame containing 0s.
1106+
_, _ = rand.Read(sc.sentPingData[:])
1107+
sc.writeFrame(FrameWriteRequest{
1108+
write: &writePing{data: sc.sentPingData},
1109+
})
1110+
pingTimeout := sc.srv.PingTimeout
1111+
if pingTimeout <= 0 {
1112+
pingTimeout = 15 * time.Second
1113+
}
1114+
sc.readIdleTimer.Reset(pingTimeout)
1115+
}
1116+
10641117
type serverMessage int
10651118

10661119
// Message values sent to serveMsgCh.
10671120
var (
10681121
settingsTimerMsg = new(serverMessage)
10691122
idleTimerMsg = new(serverMessage)
1123+
readIdleTimerMsg = new(serverMessage)
10701124
shutdownTimerMsg = new(serverMessage)
10711125
gracefulShutdownMsg = new(serverMessage)
10721126
handlerDoneMsg = new(serverMessage)
10731127
)
10741128

10751129
func (sc *serverConn) onSettingsTimer() { sc.sendServeMsg(settingsTimerMsg) }
10761130
func (sc *serverConn) onIdleTimer() { sc.sendServeMsg(idleTimerMsg) }
1131+
func (sc *serverConn) onReadIdleTimer() { sc.sendServeMsg(readIdleTimerMsg) }
10771132
func (sc *serverConn) onShutdownTimer() { sc.sendServeMsg(shutdownTimerMsg) }
10781133

10791134
func (sc *serverConn) sendServeMsg(msg interface{}) {
@@ -1604,6 +1659,11 @@ func (sc *serverConn) processFrame(f Frame) error {
16041659
func (sc *serverConn) processPing(f *PingFrame) error {
16051660
sc.serveG.check()
16061661
if f.IsAck() {
1662+
if sc.pingSent && sc.sentPingData == f.Data {
1663+
// This is a response to a PING we sent.
1664+
sc.pingSent = false
1665+
sc.readIdleTimer.Reset(sc.srv.ReadIdleTimeout)
1666+
}
16071667
// 6.7 PING: " An endpoint MUST NOT respond to PING frames
16081668
// containing this flag."
16091669
return nil

‎http2/server_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -4706,3 +4706,46 @@ func TestServerWriteByteTimeout(t *testing.T) {
47064706
st.advance(1 * time.Second) // timeout after failing to write any more bytes
47074707
st.wantClosed()
47084708
}
4709+
4710+
func TestServerPingSent(t *testing.T) {
4711+
const readIdleTimeout = 15 * time.Second
4712+
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
4713+
}, func(s *Server) {
4714+
s.ReadIdleTimeout = readIdleTimeout
4715+
})
4716+
st.greet()
4717+
4718+
st.wantIdle()
4719+
4720+
st.advance(readIdleTimeout)
4721+
_ = readFrame[*PingFrame](t, st)
4722+
st.wantIdle()
4723+
4724+
st.advance(14 * time.Second)
4725+
st.wantIdle()
4726+
st.advance(1 * time.Second)
4727+
st.wantClosed()
4728+
}
4729+
4730+
func TestServerPingResponded(t *testing.T) {
4731+
const readIdleTimeout = 15 * time.Second
4732+
st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
4733+
}, func(s *Server) {
4734+
s.ReadIdleTimeout = readIdleTimeout
4735+
})
4736+
st.greet()
4737+
4738+
st.wantIdle()
4739+
4740+
st.advance(readIdleTimeout)
4741+
pf := readFrame[*PingFrame](t, st)
4742+
st.wantIdle()
4743+
4744+
st.advance(14 * time.Second)
4745+
st.wantIdle()
4746+
4747+
st.writePing(true, pf.Data)
4748+
4749+
st.advance(2 * time.Second)
4750+
st.wantIdle()
4751+
}

‎http2/write.go

+10
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ func (se StreamError) writeFrame(ctx writeContext) error {
131131

132132
func (se StreamError) staysWithinBuffer(max int) bool { return frameHeaderLen+4 <= max }
133133

134+
type writePing struct {
135+
data [8]byte
136+
}
137+
138+
func (w writePing) writeFrame(ctx writeContext) error {
139+
return ctx.Framer().WritePing(false, w.data)
140+
}
141+
142+
func (w writePing) staysWithinBuffer(max int) bool { return frameHeaderLen+len(w.data) <= max }
143+
134144
type writePingAck struct{ pf *PingFrame }
135145

136146
func (w writePingAck) writeFrame(ctx writeContext) error {

0 commit comments

Comments
 (0)
Please sign in to comment.