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: default avatarIan Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent 0d52c144
...@@ -198,8 +198,8 @@ func (s *Server) ExportAllConnsIdle() bool { ...@@ -198,8 +198,8 @@ func (s *Server) ExportAllConnsIdle() bool {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
for c := range s.activeConn { for c := range s.activeConn {
st, ok := c.curState.Load().(ConnState) st, unixSec := c.getState()
if !ok || st != StateIdle { if unixSec == 0 || st != StateIdle {
return false return false
} }
} }
......
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
"flag"
"fmt" "fmt"
"internal/testenv" "internal/testenv"
"io" "io"
...@@ -5562,6 +5563,64 @@ func testServerShutdown(t *testing.T, h2 bool) { ...@@ -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. // Issue 17878: tests that we can call Close twice.
func TestServerCloseDeadlock(t *testing.T) { func TestServerCloseDeadlock(t *testing.T) {
var s Server var s Server
......
...@@ -283,7 +283,7 @@ type conn struct { ...@@ -283,7 +283,7 @@ type conn struct {
curReq atomic.Value // of *response (which has a Request in it) 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 guards hijackedv
mu sync.Mutex mu sync.Mutex
...@@ -1679,21 +1679,19 @@ func (c *conn) setState(nc net.Conn, state ConnState) { ...@@ -1679,21 +1679,19 @@ func (c *conn) setState(nc net.Conn, state ConnState) {
case StateHijacked, StateClosed: case StateHijacked, StateClosed:
srv.trackConn(c, false) 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 { if hook := srv.ConnState; hook != nil {
hook(nc, state) hook(nc, state)
} }
} }
// connStateInterface is an array of the interface{} versions of func (c *conn) getState() (state ConnState, unixSec int64) {
// ConnState values, so we can use them in atomic.Values later without packedState := atomic.LoadUint64(&c.curState.atomic)
// paying the cost of shoving their integers in an interface{}. return ConnState(packedState & 0xff), int64(packedState >> 8)
var connStateInterface = [...]interface{}{
StateNew: StateNew,
StateActive: StateActive,
StateIdle: StateIdle,
StateHijacked: StateHijacked,
StateClosed: StateClosed,
} }
// badRequestError is a literal string (used by in the server in HTML, // badRequestError is a literal string (used by in the server in HTML,
...@@ -2624,8 +2622,16 @@ func (s *Server) closeIdleConns() bool { ...@@ -2624,8 +2622,16 @@ func (s *Server) closeIdleConns() bool {
defer s.mu.Unlock() defer s.mu.Unlock()
quiescent := true quiescent := true
for c := range s.activeConn { for c := range s.activeConn {
st, ok := c.curState.Load().(ConnState) st, unixSec := c.getState()
if !ok || st != StateIdle { // 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 quiescent = false
continue 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