internal/jsonrpc2_v2: eliminate a temporary connection leak in (*Server).run

Prior to this CL, (*Server).run only filters out inactive connections
when it accepts a new connection. If existing connections complete,
their associated resources can't be garbage-collected until either the
next connection is accepted or the Listener is closed.

This change moves the open-connection accounting to an explicit hook
passed to newConnection, eliminating the need to call Wait entirely.

For golang/go#46047

Change-Id: I3732cb463fcea0c142f17f2b1510fdfd2dbc81da
Reviewed-on: https://go-review.googlesource.com/c/tools/+/388774
Auto-Submit: Bryan Mills <bcmills@google.com>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Bryan C. Mills 2022-03-01 12:04:14 -05:00 • committed by Gopher Robot
commit a7abf540a7
2 changed files with 22 additions and 31 deletions

View file

@ -74,6 +74,7 @@ type Connection struct {
handler Handler handler Handler
onInternalError func(error) onInternalError func(error)
onDone func()
} }
// inFlightState records the state of the incoming and outgoing calls on a // inFlightState records the state of the incoming and outgoing calls on a
@ -113,6 +114,9 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) {
idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning
if idle && (s.closing || s.readErr != nil) && !s.closed { if idle && (s.closing || s.readErr != nil) && !s.closed {
c.closeErr <- c.closer.Close() c.closeErr <- c.closer.Close()
if c.onDone != nil {
c.onDone()
}
s.closed = true s.closed = true
} }
} }
@ -131,8 +135,13 @@ func (o ConnectionOptions) Bind(context.Context, *Connection) (ConnectionOptions
} }
// newConnection creates a new connection and runs it. // newConnection creates a new connection and runs it.
//
// This is used by the Dial and Serve functions to build the actual connection. // This is used by the Dial and Serve functions to build the actual connection.
func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder) (*Connection, error) { //
// The connection is closed automatically (and its resources cleaned up) when
// the last request has completed after the underlying ReadWriteCloser breaks,
// but it may be stopped earlier by calling Close (for a clean shutdown).
func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder, onDone func()) (*Connection, error) {
// TODO: Should we create a new event span here? // TODO: Should we create a new event span here?
// This will propagate cancellation from ctx; should it? // This will propagate cancellation from ctx; should it?
ctx := notDone{bindCtx} ctx := notDone{bindCtx}
@ -141,6 +150,7 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde
closer: rwc, closer: rwc,
closeErr: make(chan error, 1), closeErr: make(chan error, 1),
writer: make(chan Writer, 1), writer: make(chan Writer, 1),
onDone: onDone,
} }
options, err := binder.Bind(bindCtx, c) options, err := binder.Bind(bindCtx, c)

View file

@ -54,7 +54,7 @@ func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newConnection(ctx, rwc, binder) return newConnection(ctx, rwc, binder, nil)
} }
// Serve starts a new server listening for incoming connections and returns // Serve starts a new server listening for incoming connections and returns
@ -84,25 +84,25 @@ func (s *Server) Wait() error {
// duration, otherwise it exits only on error. // duration, otherwise it exits only on error.
func (s *Server) run(ctx context.Context) { func (s *Server) run(ctx context.Context) {
defer s.async.done() defer s.async.done()
var activeConns []*Connection
var activeConns sync.WaitGroup
for { for {
// we never close the accepted connection, we rely on the other end // We never close the accepted connection — we rely on the other end
// closing or the socket closing itself naturally // closing or the socket closing itself naturally.
rwc, err := s.listener.Accept(ctx) rwc, err := s.listener.Accept(ctx)
if err != nil { if err != nil {
if !isClosingError(err) { if !isClosingError(err) {
s.async.setError(err) s.async.setError(err)
} }
// we are done generating new connections for good // We are done generating new connections for good.
break break
} }
// see if any connections were closed while we were waiting // A new inbound connection.
activeConns = onlyActive(activeConns) activeConns.Add(1)
_, err = newConnection(ctx, rwc, s.binder, activeConns.Done)
// a new inbound connection,
conn, err := newConnection(ctx, rwc, s.binder)
if err != nil { if err != nil {
activeConns.Done()
if !isClosingError(err) { if !isClosingError(err) {
s.async.setError(err) s.async.setError(err)
s.listener.Close() s.listener.Close()
@ -110,27 +110,8 @@ func (s *Server) run(ctx context.Context) {
} }
continue continue
} }
activeConns = append(activeConns, conn)
} }
activeConns.Wait()
// wait for all active conns to finish
for _, c := range activeConns {
c.Wait()
}
}
func onlyActive(conns []*Connection) []*Connection {
i := 0
for _, c := range conns {
c.updateInFlight(func(s *inFlightState) {
if !s.closed {
conns[i] = c
i++
}
})
}
// trim the slice down
return conns[:i]
} }
// isClosingError reports if the error occurs normally during the process of // isClosingError reports if the error occurs normally during the process of