Commit a79fe535 authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: make Server.Shutdown treat new connections as idle after 5 seconds

The Server distinguishes "new" vs "idle" connections. A TCP connection
from which no bytes have yet been written is "new". A connection that
has previously served a request and is in "keep-alive" state while
waiting for a second or further request is "idle".

The graceful Server.Shutdown historically only shut down "idle"
connections, with the assumption that a "new" connection was about to
read its request and would then shut down on its own afterwards.

But apparently some clients spin up connections and don't end up using
them, so we have something that's "new" to us, but browsers or other
clients are treating as "idle" to them.

This CL tweaks our heuristic to treat a StateNew connection as
StateIdle if it's been stuck in StateNew for over 5 seconds.

Fixes #22682

Change-Id: I01ba59a6ab67755ca5ab567041b1f54aa7b7da6f
Reviewed-on: https://go-review.googlesource.com/121419
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
Reviewed-by: 's avatarIan Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 0d52c144
......@@ -198,8 +198,8 @@ func (s *Server) ExportAllConnsIdle() bool {
s.mu.Lock()
defer s.mu.Unlock()
for c := range s.activeConn {
st, ok := c.curState.Load().(ConnState)
if !ok || st != StateIdle {
st, unixSec := c.getState()
if unixSec == 0 || st != StateIdle {
return false
}
}
......
......@@ -14,6 +14,7 @@ import (
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"internal/testenv"
"io"
......@@ -5562,6 +5563,64 @@ func testServerShutdown(t *testing.T, h2 bool) {
}
}
var slowTests = flag.Bool("slow", false, "run slow tests")
func TestServerShutdownStateNew(t *testing.T) {
if !*slowTests {
t.Skip("skipping slow test without -slow flag")
}
setParallel(t)
defer afterTest(t)
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
// nothing.
}))
defer ts.Close()
// Start a connection but never write to it.
c, err := net.Dial("tcp", ts.Listener.Addr().String())
if err != nil {
t.Fatal(err)
}
defer c.Close()
shutdownRes := make(chan error, 1)
go func() {
shutdownRes <- ts.Config.Shutdown(context.Background())
}()
readRes := make(chan error, 1)
go func() {
_, err := c.Read([]byte{0})
readRes <- err
}()
const expectTimeout = 5 * time.Second
t0 := time.Now()
select {
case got := <-shutdownRes:
d := time.Since(t0)
if got != nil {
t.Fatalf("shutdown error after %v: %v", d, err)
}
if d < expectTimeout/2 {
t.Errorf("shutdown too soon after %v", d)
}
case <-time.After(expectTimeout * 3 / 2):
t.Fatalf("timeout waiting for shutdown")
}
// Wait for c.Read to unblock; should be already done at this point,
// or within a few milliseconds.
select {
case err := <-readRes:
if err == nil {
t.Error("expected error from Read")
}
case <-time.After(2 * time.Second):
t.Errorf("timeout waiting for Read to unblock")
}
}
// Issue 17878: tests that we can call Close twice.
func TestServerCloseDeadlock(t *testing.T) {
var s Server
......
......@@ -283,7 +283,7 @@ type conn struct {
curReq atomic.Value // of *response (which has a Request in it)
curState atomic.Value // of ConnState
curState struct{ atomic uint64 } // packed (unixtime<<8|uint8(ConnState))
// mu guards hijackedv
mu sync.Mutex
......@@ -1679,21 +1679,19 @@ func (c *conn) setState(nc net.Conn, state ConnState) {
case StateHijacked, StateClosed:
srv.trackConn(c, false)
}
c.curState.Store(connStateInterface[state])
if state > 0xff || state < 0 {
panic("internal error")
}
packedState := uint64(time.Now().Unix()<<8) | uint64(state)
atomic.StoreUint64(&c.curState.atomic, packedState)
if hook := srv.ConnState; hook != nil {
hook(nc, state)
}
}
// connStateInterface is an array of the interface{} versions of
// ConnState values, so we can use them in atomic.Values later without
// paying the cost of shoving their integers in an interface{}.
var connStateInterface = [...]interface{}{
StateNew: StateNew,
StateActive: StateActive,
StateIdle: StateIdle,
StateHijacked: StateHijacked,
StateClosed: StateClosed,
func (c *conn) getState() (state ConnState, unixSec int64) {
packedState := atomic.LoadUint64(&c.curState.atomic)
return ConnState(packedState & 0xff), int64(packedState >> 8)
}
// badRequestError is a literal string (used by in the server in HTML,
......@@ -2624,8 +2622,16 @@ func (s *Server) closeIdleConns() bool {
defer s.mu.Unlock()
quiescent := true
for c := range s.activeConn {
st, ok := c.curState.Load().(ConnState)
if !ok || st != StateIdle {
st, unixSec := c.getState()
// Issue 22682: treat StateNew connections as if
// they're idle if we haven't read the first request's
// header in over 5 seconds.
if st == StateNew && unixSec < time.Now().Unix()-5 {
st = StateIdle
}
if st != StateIdle || unixSec == 0 {
// Assume unixSec == 0 means it's a very new
// connection, without state set yet.
quiescent = false
continue
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment