Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: close idle connections when server shutdown #1155

Merged
merged 4 commits into from Nov 13, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 31 additions & 1 deletion server.go
Expand Up @@ -414,9 +414,12 @@ type Server struct {
writerPool sync.Pool
hijackConnPool sync.Pool

// We need to know our listeners so we can close them in Shutdown().
// We need to know our listeners and idle connections so we can close them in Shutdown().
ln []net.Listener

idleConns map[net.Conn]struct{}
idleConnsMu sync.Mutex

mu sync.Mutex
open int32
stop int32
Expand Down Expand Up @@ -1835,6 +1838,8 @@ func (s *Server) Shutdown() error {
close(s.done)
}

s.closeIdleConns()

// Closing the listener will make Serve() call Stop on the worker pool.
// Setting .stop to 1 will make serveConn() break out of its loop.
// Now we just have to wait until all workers are done.
Expand Down Expand Up @@ -2425,6 +2430,7 @@ func (s *Server) serveConn(c net.Conn) (err error) {
}

func (s *Server) setState(nc net.Conn, state ConnState) {
s.trackConn(nc, state)
if hook := s.ConnState; hook != nil {
hook(nc, state)
}
Expand Down Expand Up @@ -2794,6 +2800,30 @@ func (s *Server) writeErrorResponse(bw *bufio.Writer, ctx *RequestCtx, serverNam
return bw
}

func (s *Server) trackConn(c net.Conn, state ConnState) {
s.idleConnsMu.Lock()
switch state {
case StateIdle:
if s.idleConns == nil {
s.idleConns = make(map[net.Conn]struct{})
}
s.idleConns[c] = struct{}{}

default:
delete(s.idleConns, c)
}
s.idleConnsMu.Unlock()
}

func (s *Server) closeIdleConns() {
s.idleConnsMu.Lock()
for c := range s.idleConns {
c.Close()
erikdubbelboer marked this conversation as resolved.
Show resolved Hide resolved
}
s.idleConns = nil
s.idleConnsMu.Unlock()
}

// A ConnState represents the state of a client connection to a server.
// It's used by the optional Server.ConnState hook.
type ConnState int
Expand Down
43 changes: 43 additions & 0 deletions server_test.go
Expand Up @@ -3347,6 +3347,49 @@ func TestShutdownErr(t *testing.T) {
verifyResponse(t, br, StatusOK, "aaa/bbb", "real response")
}

func TestShutdownCloseIdleConns(t *testing.T) {
t.Parallel()

ln := fasthttputil.NewInmemoryListener()
s := &Server{
Handler: func(ctx *RequestCtx) {
ctx.Success("aaa/bbb", []byte("real response"))
},
}
go func() {
if err := s.Serve(ln); err != nil {
t.Errorf("unexepcted error: %s", err)
}
}()
conn, err := ln.Dial()
if err != nil {
t.Fatalf("unexepcted error: %s", err)
}

if _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")); err != nil {
t.Errorf("unexpected error: %s", err)
}
time.Sleep(100 * time.Millisecond)
erikdubbelboer marked this conversation as resolved.
Show resolved Hide resolved

shutdownErr := make(chan error)
go func() {
shutdownErr <- s.Shutdown()
}()

timer := time.NewTimer(time.Second)
select {
case <-timer.C:
t.Fatalf("idel connections don't close when shutdown")
erikdubbelboer marked this conversation as resolved.
Show resolved Hide resolved
case err = <-shutdownErr:
if err != nil {
t.Errorf("unexepcted error: %s", err)
}
}

br := bufio.NewReader(conn)
verifyResponse(t, br, StatusOK, "aaa/bbb", "real response")
}

func TestMultipleServe(t *testing.T) {
t.Parallel()

Expand Down