internal/jsonrpc2_v2: move the idle timeout handling out of the server

Change-Id: Ia7dedd84171f4fc4818fb00b154c21b04887391f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/305129
Trust: Ian Cottrell <iancottrell@google.com>
Run-TryBot: Ian Cottrell <iancottrell@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Ian Cottrell 2021-03-25 22:16:30 -04:00
commit 76e263b8c9
4 changed files with 163 additions and 101 deletions

View file

@ -68,6 +68,15 @@ func (a *async) done() {
close(a.ready) close(a.ready)
} }
func (a *async) isDone() bool {
select {
case <-a.ready:
return true
default:
return false
}
}
func (a *async) wait() error { func (a *async) wait() error {
<-a.ready <-a.ready
err := <-a.errBox err := <-a.errBox

View file

@ -130,7 +130,7 @@ func testConnection(t *testing.T, framer jsonrpc2.Framer) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
server, err := jsonrpc2.Serve(ctx, listener, binder{framer, nil}, jsonrpc2.ServeOptions{}) server, err := jsonrpc2.Serve(ctx, listener, binder{framer, nil})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View file

@ -7,9 +7,9 @@ package jsonrpc2
import ( import (
"context" "context"
"io" "io"
"sync"
"time" "time"
"golang.org/x/tools/internal/event"
errors "golang.org/x/xerrors" errors "golang.org/x/xerrors"
) )
@ -39,17 +39,9 @@ type Dialer interface {
type Server struct { type Server struct {
listener Listener listener Listener
binder Binder binder Binder
options ServeOptions // a copy of the config that started this server
async async async async
} }
// ServeOptions holds the options to the Serve function.
//TODO: kill ServeOptions and push timeout into the listener
type ServeOptions struct {
// IdleTimeout is the maximum amount of time to remain idle and running.
IdleTimeout time.Duration
}
// Dial uses the dialer to make a new connection, wraps the returned // Dial uses the dialer to make a new connection, wraps the returned
// reader and writer using the framer to make a stream, and then builds // reader and writer using the framer to make a stream, and then builds
// a connection on top of that stream using the binder. // a connection on top of that stream using the binder.
@ -69,11 +61,10 @@ func Dial(ctx context.Context, dialer Dialer, binder Binder) (*Connection, error
// You can call Wait to block on the server, or Shutdown to get the sever to // You can call Wait to block on the server, or Shutdown to get the sever to
// terminate gracefully. // terminate gracefully.
// To notice incoming connections, use an intercepting Binder. // To notice incoming connections, use an intercepting Binder.
func Serve(ctx context.Context, listener Listener, binder Binder, options ServeOptions) (*Server, error) { func Serve(ctx context.Context, listener Listener, binder Binder) (*Server, error) {
server := &Server{ server := &Server{
listener: listener, listener: listener,
binder: binder, binder: binder,
options: options,
} }
server.async.init() server.async.init()
go server.run(ctx) go server.run(ctx)
@ -90,94 +81,49 @@ 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()
// Max duration: ~290 years; surely that's long enough. var activeConns []*Connection
const forever = 1<<63 - 1
idleTimeout := s.options.IdleTimeout
if idleTimeout <= 0 {
idleTimeout = forever
}
idleTimer := time.NewTimer(idleTimeout)
// run a goroutine that listens for incoming connections and posts them
// back to the worker
newStreams := make(chan io.ReadWriteCloser)
go func() {
for {
// we never close the accepted connection, we rely on the other end
// closing or the socket closing itself naturally
rwc, err := s.listener.Accept(ctx)
if err != nil {
if !isClosingError(err) {
event.Error(ctx, "Accept", err)
}
// signal we are done generating new connections for good
close(newStreams)
return
}
newStreams <- rwc
}
}()
closedConns := make(chan struct{})
activeConns := 0
lnClosed := false
for { for {
select { // we never close the accepted connection, we rely on the other end
case rwc := <-newStreams: // closing or the socket closing itself naturally
// whatever happes we are not idle anymore rwc, err := s.listener.Accept(ctx)
idleTimer.Stop() if err != nil {
if rwc == nil { if !isClosingError(err) {
// the net listener has been closed s.async.setError(err)
lnClosed = true
if activeConns == 0 {
// accept is done and there are no active connections, so just stop now
return
}
// replace the channel with one that will never trigger
// this is save because the only writer has already quit
newStreams = nil
// and then wait for all active connections to stop
continue
} }
// a new inbound connection, // we are done generating new connections for good
conn, err := newConnection(ctx, rwc, s.binder) break
if err != nil { }
if !isClosingError(err) {
event.Error(ctx, "NewConn", err) // see if any connections were closed while we were waiting
} activeConns = onlyActive(activeConns)
continue
// a new inbound connection,
conn, err := newConnection(ctx, rwc, s.binder)
if err != nil {
if !isClosingError(err) {
s.async.setError(err)
} }
// register the new conn as active continue
activeConns++ }
// wrap the conn in a close monitor activeConns = append(activeConns, conn)
//TODO: we do this to maintain our active count correctly, is there a better way? }
go func() {
err := conn.Wait() // wait for all active conns to finish
if err != nil && !isClosingError(err) { for _, c := range activeConns {
event.Error(ctx, "closed a connection", err) c.Wait()
} }
closedConns <- struct{}{} }
}()
case <-closedConns: func onlyActive(conns []*Connection) []*Connection {
activeConns-- i := 0
if activeConns == 0 { for _, c := range conns {
// no more active connections, restart the idle timer if !c.async.isDone() {
if lnClosed { conns[i] = c
// we can never get a new connection, so we are done i++
return
}
// we are idle, but might get a new connection still
idleTimer.Reset(idleTimeout)
}
case <-idleTimer.C:
// no activity for a while, time to stop serving
s.async.setError(ErrIdleTimeout)
return
case <-ctx.Done():
s.async.setError(ctx.Err())
return
} }
} }
// 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
@ -206,3 +152,114 @@ func isClosingError(err error) bool {
return false return false
} }
// NewIdleListener wraps a listener with an idle timeout.
// When there are no active connections for at least the timeout duration a
// call to accept will fail with ErrIdleTimeout.
func NewIdleListener(timeout time.Duration, wrap Listener) Listener {
l := &idleListener{
timeout: timeout,
wrapped: wrap,
newConns: make(chan *idleCloser),
closed: make(chan struct{}),
wasTimeout: make(chan struct{}),
}
go l.run()
return l
}
type idleListener struct {
wrapped Listener
timeout time.Duration
newConns chan *idleCloser
closed chan struct{}
wasTimeout chan struct{}
closeOnce sync.Once
}
type idleCloser struct {
wrapped io.ReadWriteCloser
closed chan struct{}
closeOnce sync.Once
}
func (c *idleCloser) Read(p []byte) (int, error) {
n, err := c.wrapped.Read(p)
if err != nil && isClosingError(err) {
c.closeOnce.Do(func() { close(c.closed) })
}
return n, err
}
func (c *idleCloser) Write(p []byte) (int, error) {
// we do not close on write failure, we rely on the wrapped writer to do that
// if it is appropriate, which we will detect in the next read.
return c.wrapped.Write(p)
}
func (c *idleCloser) Close() error {
// we rely on closing the wrapped stream to signal to the next read that we
// are closed, rather than triggering the closed signal directly
return c.wrapped.Close()
}
func (l *idleListener) Accept(ctx context.Context) (io.ReadWriteCloser, error) {
rwc, err := l.wrapped.Accept(ctx)
if err != nil {
if isClosingError(err) {
// underlying listener was closed
l.closeOnce.Do(func() { close(l.closed) })
// was it closed because of the idle timeout?
select {
case <-l.wasTimeout:
err = ErrIdleTimeout
default:
}
}
return nil, err
}
conn := &idleCloser{
wrapped: rwc,
closed: make(chan struct{}),
}
l.newConns <- conn
return conn, err
}
func (l *idleListener) Close() error {
defer l.closeOnce.Do(func() { close(l.closed) })
return l.wrapped.Close()
}
func (l *idleListener) Dialer() Dialer {
return l.wrapped.Dialer()
}
func (l *idleListener) run() {
var conns []*idleCloser
for {
var firstClosed chan struct{} // left at nil if there are no active conns
var timeout <-chan time.Time // left at nil if there are active conns
if len(conns) > 0 {
firstClosed = conns[0].closed
} else {
timeout = time.After(l.timeout)
}
select {
case <-l.closed:
// the main listener closed, no need to keep going
return
case conn := <-l.newConns:
// a new conn arrived, add it to the list
conns = append(conns, conn)
case <-timeout:
// we timed out, only happens when there are no active conns
// close the underlying listener, and allow the normal closing process to happen
close(l.wasTimeout)
l.wrapped.Close()
case <-firstClosed:
// a conn closed, remove it from the active list
conns = conns[:copy(conns, conns[1:])]
}
}
}

View file

@ -23,12 +23,9 @@ func TestIdleTimeout(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
listener = jsonrpc2.NewIdleListener(100*time.Millisecond, listener)
defer listener.Close() defer listener.Close()
server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{})
server, err := jsonrpc2.Serve(ctx, listener, jsonrpc2.ConnectionOptions{},
jsonrpc2.ServeOptions{
IdleTimeout: 100 * time.Millisecond,
})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -119,10 +116,9 @@ func TestServe(t *testing.T) {
} }
func newFake(ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(context.Context), error) { func newFake(ctx context.Context, l jsonrpc2.Listener) (*jsonrpc2.Connection, func(context.Context), error) {
l = jsonrpc2.NewIdleListener(100*time.Millisecond, l)
server, err := jsonrpc2.Serve(ctx, l, jsonrpc2.ConnectionOptions{ server, err := jsonrpc2.Serve(ctx, l, jsonrpc2.ConnectionOptions{
Handler: fakeHandler{}, Handler: fakeHandler{},
}, jsonrpc2.ServeOptions{
IdleTimeout: 100 * time.Millisecond,
}) })
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err