mirror of https://github.com/golang/go.git
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:
parent
a208025ccb
commit
9ffc0ab4ef
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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{}}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue