internal/jsonrpc2: add an idle timeout for stream serving

When running gopls against an automatically started remote instance, we
want the lifecycle of the remote to be detached from that of its
clients, so that it doesn't shut down while clients are still connected.
On the other hand, a gopls process can consume significant resources, so
we don't want it to remain when there are no more connected clients.

The jsonrpc2 package is updated to support the concept of idle timeout:
a duration after which the server is shut down when there are no
connected clients. This is exposed in the gopls serve command via the
-listen.timeout flag.

Update golang/go#34111

Change-Id: Id62b3d4a2fa66de2c9306d130ca431717f01d1e5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/220281
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
This commit is contained in:
Rob Findley 2020-02-19 18:48:38 -05:00 committed by Robert Findley
parent a208025ccb
commit 9ffc0ab4ef
5 changed files with 131 additions and 28 deletions

View File

@ -6,9 +6,12 @@ package jsonrpc2
import (
"context"
"errors"
"fmt"
"log"
"net"
"os"
"time"
)
// NOTE: This file provides an experimental API for serving multiple remote
@ -41,32 +44,72 @@ func HandlerServer(h Handler) StreamServer {
})
}
// ListenAndServe starts an jsonrpc2 server on the given address. It exits only
// on error.
func ListenAndServe(ctx context.Context, network, addr string, server StreamServer) error {
// ListenAndServe starts an jsonrpc2 server on the given address. If
// idleTimeout is non-zero, ListenAndServe exits after there are no clients for
// this duration, otherwise it exits only on error.
func ListenAndServe(ctx context.Context, network, addr string, server StreamServer, idleTimeout time.Duration) error {
ln, err := net.Listen(network, addr)
if err != nil {
return err
}
defer ln.Close()
if network == "unix" {
defer os.Remove(addr)
}
return Serve(ctx, ln, server)
return Serve(ctx, ln, server, idleTimeout)
}
// ErrIdleTimeout is returned when serving timed out waiting for new connections.
var ErrIdleTimeout = errors.New("timed out waiting for new connections")
// Serve accepts incoming connections from the network, and handles them using
// the provided server. It exits only on error.
func Serve(ctx context.Context, ln net.Listener, server StreamServer) error {
for {
netConn, err := ln.Accept()
if err != nil {
return err
}
stream := NewHeaderStream(netConn, netConn)
go func() {
if err := server.ServeStream(ctx, stream); err != nil {
log.Printf("serving stream: %v", err)
// the provided server. If idleTimeout is non-zero, ListenAndServe exits after
// there are no clients for this duration, otherwise it exits only on error.
func Serve(ctx context.Context, ln net.Listener, server StreamServer, idleTimeout time.Duration) error {
// Max duration: ~290 years; surely that's long enough.
const forever = 1<<63 - 1
if idleTimeout <= 0 {
idleTimeout = forever
}
connTimer := time.NewTimer(idleTimeout)
newConns := make(chan net.Conn)
doneListening := make(chan error)
closedConns := make(chan error)
go func() {
for {
nc, err := ln.Accept()
if err != nil {
doneListening <- fmt.Errorf("Accept(): %v", err)
return
}
}()
newConns <- nc
}
}()
activeConns := 0
for {
select {
case netConn := <-newConns:
activeConns++
connTimer.Stop()
stream := NewHeaderStream(netConn, netConn)
go func() {
closedConns <- server.ServeStream(ctx, stream)
}()
case err := <-doneListening:
return err
case err := <-closedConns:
log.Printf("closed a connection with error: %v", err)
activeConns--
if activeConns == 0 {
connTimer.Reset(idleTimeout)
}
case <-connTimer.C:
return ErrIdleTimeout
case <-ctx.Done():
return ctx.Err()
}
}
}

View File

@ -0,0 +1,59 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package jsonrpc2
import (
"context"
"net"
"sync"
"testing"
"time"
)
func TestIdleTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ln, err := net.Listen("tcp", ":0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
connect := func() net.Conn {
conn, err := net.DialTimeout("tcp", ln.Addr().String(), 5*time.Second)
if err != nil {
panic(err)
}
return conn
}
server := HandlerServer(EmptyHandler{})
// connTimer := &fakeTimer{c: make(chan time.Time, 1)}
var (
runErr error
wg sync.WaitGroup
)
wg.Add(1)
go func() {
defer wg.Done()
runErr = Serve(ctx, ln, server, 100*time.Millisecond)
}()
// Exercise some connection/disconnection patterns, and then assert that when
// our timer fires, the server exits.
conn1 := connect()
conn2 := connect()
conn1.Close()
conn2.Close()
conn3 := connect()
conn3.Close()
wg.Wait()
if runErr != ErrIdleTimeout {
t.Errorf("run() returned error %v, want %v", runErr, ErrIdleTimeout)
}
}

View File

@ -39,7 +39,7 @@ func NewTCPServer(ctx context.Context, server jsonrpc2.StreamServer) *TCPServer
if err != nil {
panic(fmt.Sprintf("servertest: failed to listen: %v", err))
}
go jsonrpc2.Serve(ctx, ln, server)
go jsonrpc2.Serve(ctx, ln, server, 0)
return &TCPServer{Addr: ln.Addr().String(), ln: ln, cls: &closerList{}}
}

View File

@ -36,11 +36,10 @@ func TestTestServer(t *testing.T) {
pipeTS := NewPipeServer(ctx, server)
defer pipeTS.Close()
tests := []struct {
name string
name string
connector Connector
} {
}{
{"tcp", tcpTS},
{"pipe", pipeTS},
}

View File

@ -10,6 +10,7 @@ import (
"fmt"
"os"
"strings"
"time"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/lsp/cache"
@ -21,12 +22,13 @@ import (
// Serve is a struct that exposes the configurable parts of the LSP server as
// flags, in the right form for tool.Main to consume.
type Serve struct {
Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"`
Mode string `flag:"mode" help:"no effect"`
Port int `flag:"port" help:"port on which to run gopls for debugging purposes"`
Address string `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."`
Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"`
Debug string `flag:"debug" help:"serve debug information on the supplied address"`
Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"`
Mode string `flag:"mode" help:"no effect"`
Port int `flag:"port" help:"port on which to run gopls for debugging purposes"`
Address string `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."`
IdleTimeout time.Duration `flag:"listen.timeout" help:"when used with -listen, shut down the server when there are no connected clients for this duration"`
Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"`
Debug string `flag:"debug" help:"serve debug information on the supplied address"`
app *Application
}
@ -73,11 +75,11 @@ func (s *Serve) Run(ctx context.Context, args ...string) error {
if s.Address != "" {
network, addr := parseAddr(s.Address)
return jsonrpc2.ListenAndServe(ctx, network, addr, ss)
return jsonrpc2.ListenAndServe(ctx, network, addr, ss, s.IdleTimeout)
}
if s.Port != 0 {
addr := fmt.Sprintf(":%v", s.Port)
return jsonrpc2.ListenAndServe(ctx, "tcp", addr, ss)
return jsonrpc2.ListenAndServe(ctx, "tcp", addr, ss, s.IdleTimeout)
}
stream := jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout)
if s.Trace {