Skip to content

Commit

Permalink
http2: do not surface errors from a conn's idle timer expiring
Browse files Browse the repository at this point in the history
CL 625398 surfaces errors occurring on a client conn before it
has been used for any requests.

Don't surface "errors" arising from a conn being closed for
idleness without ever being used.

For golang/go#70515

Change-Id: I2b45215f90f74fee66ee46f3b62d27117147c64a
Reviewed-on: https://go-review.googlesource.com/c/net/+/631815
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: David Chase <[email protected]>
Reviewed-by: Brad Fitzpatrick <[email protected]>
  • Loading branch information
neild committed Dec 30, 2024
1 parent c2be992 commit e9d95ba
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 3 deletions.
13 changes: 10 additions & 3 deletions http2/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ type ClientConn struct {
doNotReuse bool // whether conn is marked to not be reused for any future requests
closing bool
closed bool
closedOnIdle bool // true if conn was closed for idleness
seenSettings bool // true if we've seen a settings frame, false otherwise
seenSettingsChan chan struct{} // closed when seenSettings is true or frame reading fails
wantSettingsAck bool // we sent a SETTINGS frame and haven't heard back
Expand Down Expand Up @@ -1089,10 +1090,12 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) {

// If this connection has never been used for a request and is closed,
// then let it take a request (which will fail).
// If the conn was closed for idleness, we're racing the idle timer;
// don't try to use the conn. (Issue #70515.)
//
// This avoids a situation where an error early in a connection's lifetime
// goes unreported.
if cc.nextStreamID == 1 && cc.streamsReserved == 0 && cc.closed {
if cc.nextStreamID == 1 && cc.streamsReserved == 0 && cc.closed && !cc.closedOnIdle {
st.canTakeNewRequest = true
}

Expand Down Expand Up @@ -1155,6 +1158,7 @@ func (cc *ClientConn) closeIfIdle() {
return
}
cc.closed = true
cc.closedOnIdle = true
nextID := cc.nextStreamID
// TODO: do clients send GOAWAY too? maybe? Just Close:
cc.mu.Unlock()
Expand Down Expand Up @@ -2434,9 +2438,12 @@ func (rl *clientConnReadLoop) cleanup() {
// This avoids a situation where new connections are constantly created,
// added to the pool, fail, and are removed from the pool, without any error
// being surfaced to the user.
const unusedWaitTime = 5 * time.Second
unusedWaitTime := 5 * time.Second
if cc.idleTimeout > 0 && unusedWaitTime > cc.idleTimeout {
unusedWaitTime = cc.idleTimeout
}
idleTime := cc.t.now().Sub(cc.lastActive)
if atomic.LoadUint32(&cc.atomicReused) == 0 && idleTime < unusedWaitTime {
if atomic.LoadUint32(&cc.atomicReused) == 0 && idleTime < unusedWaitTime && !cc.closedOnIdle {
cc.idleTimer = cc.t.afterFunc(unusedWaitTime-idleTime, func() {
cc.t.connPool().MarkDead(cc)
})
Expand Down
32 changes: 32 additions & 0 deletions http2/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5789,6 +5789,38 @@ func TestTransportTLSNextProtoConnImmediateFailureUsed(t *testing.T) {
}
}

// Test the case where a conn provided via a TLSNextProto hook is closed for idleness
// before we use it.
func TestTransportTLSNextProtoConnIdleTimoutBeforeUse(t *testing.T) {
t1 := &http.Transport{
IdleConnTimeout: 1 * time.Second,
}
t2, _ := ConfigureTransports(t1)
tt := newTestTransport(t, t2)

// Create a new, fake connection and pass it to the Transport via the TLSNextProto hook.
cli, _ := synctestNetPipe(tt.group)
cliTLS := tls.Client(cli, tlsConfigInsecure)
go func() {
tt.group.Join()
t1.TLSNextProto["h2"]("dummy.tld", cliTLS)
}()
tt.sync()
tc := tt.getConn()

// The connection encounters an error before we send a request that uses it.
tc.advance(2 * time.Second)

// Send a request on the Transport.
//
// It should fail with ErrNoCachedConn.
req := must(http.NewRequest("GET", "https://dummy.tld/", nil))
rt := tt.roundTrip(req)
if err := rt.err(); !errors.Is(err, ErrNoCachedConn) {
t.Fatalf("RoundTrip with conn closed for idleness: got %v, want ErrNoCachedConn", err)
}
}

// Test the case where a conn provided via a TLSNextProto hook immediately encounters an error,
// but no requests are sent which would use the bad connection.
func TestTransportTLSNextProtoConnImmediateFailureUnused(t *testing.T) {
Expand Down

0 comments on commit e9d95ba

Please sign in to comment.