Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.

Commit f9fdcbb

Browse files
authored
Evict cache entries when the connection closes (#359)
1 parent 6d238ad commit f9fdcbb

File tree

20 files changed

+1197
-45
lines changed

20 files changed

+1197
-45
lines changed

Gopkg.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

buildserver/bench_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func BenchmarkIntegration(b *testing.B) {
112112
label := strings.Replace(root.Host+root.Path, "/", "-", -1)
113113

114114
b.Run(label, func(b *testing.B) {
115-
fs, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: rootURI})
115+
fs, _, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: rootURI})
116116
if err != nil {
117117
b.Fatal(err)
118118
}
@@ -232,11 +232,11 @@ func BenchmarkIntegrationShared(b *testing.B) {
232232
ctx := context.Background()
233233
for label, test := range tests {
234234
b.Run(label, func(b *testing.B) {
235-
oldfs, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: lsp.DocumentURI(test.oldRootURI)})
235+
oldfs, _, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: lsp.DocumentURI(test.oldRootURI)})
236236
if err != nil {
237237
b.Fatal(err)
238238
}
239-
fs, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: lsp.DocumentURI(test.rootURI)})
239+
fs, _, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: lsp.DocumentURI(test.rootURI)})
240240
if err != nil {
241241
b.Fatal(err)
242242
}

buildserver/build_server.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"io"
89
"log"
910
"net/http"
1011
"net/url"
@@ -17,6 +18,8 @@ import (
1718
"sync"
1819
"time"
1920

21+
multierror "github.com/hashicorp/go-multierror"
22+
2023
"github.com/die-net/lrucache"
2124
"github.com/gregjones/httpcache"
2225
opentracing "github.com/opentracing/opentracing-go"
@@ -44,7 +47,7 @@ var Debug = true
4447
// and mapping local file system paths to logical URIs (e.g.,
4548
// /goroot/src/fmt/print.go ->
4649
// git://github.com/golang/go?go1.7.1#src/fmt/print.go).
47-
func NewHandler(defaultCfg langserver.Config) jsonrpc2.Handler {
50+
func NewHandler(defaultCfg langserver.Config) *BuildHandler {
4851
if defaultCfg.MaxParallelism <= 0 {
4952
panic(fmt.Sprintf("langserver.Config.MaxParallelism must be at least 1 (got %d)", defaultCfg.MaxParallelism))
5053
}
@@ -57,7 +60,7 @@ func NewHandler(defaultCfg langserver.Config) jsonrpc2.Handler {
5760
},
5861
}
5962
shared.FindPackage = h.findPackageCached
60-
return jsonrpc2.HandlerWithError(h.handle)
63+
return h
6164
}
6265

6366
// BuildHandler is a Go build server LSP/JSON-RPC handler that wraps a
@@ -77,6 +80,7 @@ type BuildHandler struct {
7780
init *lspext.InitializeParams // set by "initialize" request
7881
rootImportPath string // root import path of the workspace (e.g., "github.com/foo/bar")
7982
cachingClient *http.Client // http.Client with a cache backed by an in-memory LRU cache
83+
closers []io.Closer // values to dispose of when Close() is called
8084
}
8185

8286
// reset clears all internal state in h.
@@ -102,7 +106,7 @@ func (h *BuildHandler) reset(init *lspext.InitializeParams, conn *jsonrpc2.Conn,
102106
return nil
103107
}
104108

105-
func (h *BuildHandler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) {
109+
func (h *BuildHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) {
106110
// Prevent any uncaught panics from taking the entire server down.
107111
defer func() {
108112
if r := recover(); r != nil {
@@ -187,10 +191,11 @@ func (h *BuildHandler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jso
187191

188192
// Determine the root import path of this workspace (e.g., "github.com/user/repo").
189193
span.SetTag("originalRootPath", params.OriginalRootURI)
190-
fs, err := RemoteFS(ctx, params)
194+
fs, closer, err := RemoteFS(ctx, params)
191195
if err != nil {
192196
return nil, err
193197
}
198+
h.closers = append(h.closers, closer)
194199

195200
langInitParams, err := determineEnvironment(ctx, fs, params)
196201
if err != nil {
@@ -571,3 +576,15 @@ func (h *BuildHandler) callLangServer(ctx context.Context, conn *jsonrpc2.Conn,
571576
}
572577
return nil
573578
}
579+
580+
// Close implements io.Closer
581+
func (h *BuildHandler) Close() error {
582+
var result error
583+
for _, closer := range h.closers {
584+
err := closer.Close()
585+
if err != nil {
586+
result = multierror.Append(result, err)
587+
}
588+
}
589+
return result
590+
}

buildserver/integration_test.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package buildserver_test
33
import (
44
"context"
55
"fmt"
6+
"io"
7+
"io/ioutil"
68
"net/url"
79
"os"
810
"strings"
@@ -267,15 +269,16 @@ func TestIntegration(t *testing.T) {
267269
// test.
268270
func useGithubForVFS() func() {
269271
origRemoteFS := gobuildserver.RemoteFS
270-
gobuildserver.RemoteFS = func(ctx context.Context, initializeParams lspext.InitializeParams) (ctxvfs.FileSystem, error) {
272+
gobuildserver.RemoteFS = func(ctx context.Context, initializeParams lspext.InitializeParams) (ctxvfs.FileSystem, io.Closer, error) {
271273
u, err := gituri.Parse(string(initializeParams.OriginalRootURI))
272274
if err != nil {
273-
return nil, errors.Wrap(err, "could not parse workspace URI for remotefs")
275+
return nil, ioutil.NopCloser(strings.NewReader("")), errors.Wrap(err, "could not parse workspace URI for remotefs")
274276
}
275277
if u.Rev() == "" {
276-
return nil, errors.Errorf("rev is required in uri: %s", initializeParams.OriginalRootURI)
278+
return nil, ioutil.NopCloser(strings.NewReader("")), errors.Errorf("rev is required in uri: %s", initializeParams.OriginalRootURI)
277279
}
278-
return vfsutil.NewGitHubRepoVFS(ctx, string(u.Repo()), u.Rev())
280+
fs, err := vfsutil.NewGitHubRepoVFS(ctx, string(u.Repo()), u.Rev())
281+
return fs, ioutil.NopCloser(strings.NewReader("")), err
279282
}
280283

281284
return func() {

buildserver/proxy_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"io/ioutil"
89
"net/url"
910
"os"
1011
"path"
@@ -586,8 +587,8 @@ func yza() {}
586587
}()
587588

588589
origRemoteFS := gobuildserver.RemoteFS
589-
gobuildserver.RemoteFS = func(ctx context.Context, initializeParams lspext.InitializeParams) (ctxvfs.FileSystem, error) {
590-
return mapFS(test.fs), nil
590+
gobuildserver.RemoteFS = func(ctx context.Context, initializeParams lspext.InitializeParams) (ctxvfs.FileSystem, io.Closer, error) {
591+
return mapFS(test.fs), ioutil.NopCloser(strings.NewReader("")), nil
591592
}
592593

593594
defer func() {
@@ -703,7 +704,7 @@ func connectionToNewBuildServer(root string, t testing.TB) (*jsonrpc2.Conn, func
703704
// do not use the pkg cache because tests won't install any pkgs
704705
config.UseBinaryPkgCache = false
705706

706-
jsonrpc2.NewConn(context.Background(), a, jsonrpc2.AsyncHandler(gobuildserver.NewHandler(config)))
707+
jsonrpc2.NewConn(context.Background(), a, jsonrpc2.AsyncHandler(jsonrpc2.HandlerWithError(gobuildserver.NewHandler(config).Handle)))
707708

708709
conn := jsonrpc2.NewConn(context.Background(), b, NoopHandler{}, jsonrpc2.OnRecv(onRecv), jsonrpc2.OnSend(onSend))
709710
done := func() {

buildserver/stress_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import (
1313

1414
"github.com/neelance/parallel"
1515
"github.com/sourcegraph/ctxvfs"
16+
gobuildserver "github.com/sourcegraph/go-langserver/buildserver"
17+
"github.com/sourcegraph/go-langserver/gituri"
1618
"github.com/sourcegraph/go-langserver/pkg/lsp"
1719
"github.com/sourcegraph/go-lsp/lspext"
1820
"github.com/sourcegraph/jsonrpc2"
19-
gobuildserver "github.com/sourcegraph/go-langserver/buildserver"
20-
"github.com/sourcegraph/go-langserver/gituri"
2121
)
2222

2323
// BenchmarkStress benchmarks performing "textDocument/definition",
@@ -60,7 +60,7 @@ func BenchmarkStress(b *testing.B) {
6060
label := strings.Replace(root.Host+root.Path, "/", "-", -1)
6161

6262
b.Run(label, func(b *testing.B) {
63-
fs, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: rootURI})
63+
fs, _, err := gobuildserver.RemoteFS(context.Background(), lspext.InitializeParams{OriginalRootURI: rootURI})
6464
if err != nil {
6565
b.Fatal(err)
6666
}

buildserver/vfs.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package buildserver
22

33
import (
44
"context"
5+
"io"
6+
"io/ioutil"
7+
"strings"
58

69
"github.com/pkg/errors"
710
"github.com/prometheus/client_golang/prometheus"
@@ -17,7 +20,7 @@ import (
1720
// SECURITY NOTE: This DOES NOT check that the user or context has permissions
1821
// to read the repo. We assume permission checks happen before a request reaches
1922
// a build server.
20-
var RemoteFS = func(ctx context.Context, initializeParams lspext.InitializeParams) (ctxvfs.FileSystem, error) {
23+
var RemoteFS = func(ctx context.Context, initializeParams lspext.InitializeParams) (ctxvfs.FileSystem, io.Closer, error) {
2124
zipURL := func() string {
2225
initializationOptions, ok := initializeParams.InitializationOptions.(map[string]interface{})
2326
if !ok {
@@ -27,9 +30,10 @@ var RemoteFS = func(ctx context.Context, initializeParams lspext.InitializeParam
2730
return url
2831
}()
2932
if zipURL != "" {
30-
return vfsutil.NewZipVFS(ctx, zipURL, zipFetch.Inc, zipFetchFailed.Inc, true)
33+
vfs, err := vfsutil.NewZipVFS(ctx, zipURL, zipFetch.Inc, zipFetchFailed.Inc, true)
34+
return vfs, vfs, err
3135
}
32-
return nil, errors.Errorf("no zipURL was provided in the initializationOptions")
36+
return nil, ioutil.NopCloser(strings.NewReader("")), errors.Errorf("no zipURL was provided in the initializationOptions")
3337
}
3438

3539
var zipFetch = prometheus.NewCounter(prometheus.CounterOpts{

diskcache/cache.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ type Store struct {
3535
// BeforeEvict, when non-nil, is a function to call before evicting a file.
3636
// It is passed the path to the file to be evicted.
3737
BeforeEvict func(string)
38+
39+
// MaxCacheSizeBytes is the maximum size of the cache directory after evicting
40+
// entries.
41+
MaxCacheSizeBytes int64
3842
}
3943

4044
// File is an os.File, but includes the Path
@@ -184,9 +188,15 @@ type EvictStats struct {
184188
Evicted int
185189
}
186190

191+
// Evict will remove files from Store.Dir until it is smaller than
192+
// Store.MaxCacheSizeBytes. It evicts files with the oldest modification time first.
193+
func (s *Store) Evict() {
194+
s.EvictMaxSize(s.MaxCacheSizeBytes)
195+
}
196+
187197
// Evict will remove files from Store.Dir until it is smaller than
188198
// maxCacheSizeBytes. It evicts files with the oldest modification time first.
189-
func (s *Store) Evict(maxCacheSizeBytes int64) (stats EvictStats, err error) {
199+
func (s *Store) EvictMaxSize(maxCacheSizeBytes int64) (stats EvictStats, err error) {
190200
isZip := func(fi os.FileInfo) bool {
191201
return strings.HasSuffix(fi.Name(), ".zip")
192202
}

main.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"flag"
77
"fmt"
88
"io"
9+
"io/ioutil"
910
"log"
1011
"net"
1112
"net/http"
1213
"os"
1314
"path/filepath"
1415
"runtime/debug"
16+
"strings"
1517
"time"
1618

1719
"github.com/keegancsmith/tmpfriend"
@@ -33,15 +35,16 @@ import (
3335
)
3436

3537
var (
36-
mode = flag.String("mode", "stdio", "communication mode (stdio|tcp|websocket)")
37-
addr = flag.String("addr", ":4389", "server listen address (tcp or websocket)")
38-
trace = flag.Bool("trace", false, "print all requests and responses")
39-
logfile = flag.String("logfile", "", "also log to this file (in addition to stderr)")
40-
printVersion = flag.Bool("version", false, "print version and exit")
41-
pprof = flag.String("pprof", "", "start a pprof http server (https://golang.org/pkg/net/http/pprof/)")
42-
freeosmemory = flag.Bool("freeosmemory", true, "aggressively free memory back to the OS")
43-
useBuildServer = flag.Bool("usebuildserver", false, "use a build server to fetch dependencies, fetch files via Zip URL, etc.")
44-
cacheDir = flag.String("cachedir", "/tmp", "directory to store cached archives")
38+
mode = flag.String("mode", "stdio", "communication mode (stdio|tcp|websocket)")
39+
addr = flag.String("addr", ":4389", "server listen address (tcp or websocket)")
40+
trace = flag.Bool("trace", false, "print all requests and responses")
41+
logfile = flag.String("logfile", "", "also log to this file (in addition to stderr)")
42+
printVersion = flag.Bool("version", false, "print version and exit")
43+
pprof = flag.String("pprof", "", "start a pprof http server (https://golang.org/pkg/net/http/pprof/)")
44+
freeosmemory = flag.Bool("freeosmemory", true, "aggressively free memory back to the OS")
45+
useBuildServer = flag.Bool("usebuildserver", false, "use a build server to fetch dependencies, fetch files via Zip URL, etc.")
46+
cacheDir = flag.String("cachedir", "/tmp", "directory to store cached archives")
47+
maxCacheSizeBytes = flag.Int64("maxCacheSizeBytes", 50*1024*1024*1024, "the maximum size of the cache directory after evicting entries")
4548

4649
// Default Config, can be overridden by InitializationOptions
4750
usebinarypkgcache = flag.Bool("usebinarypkgcache", true, "use $GOPATH/pkg binary .a files (improves performance). Can be overridden by InitializationOptions.")
@@ -76,6 +79,7 @@ func main() {
7679
log.SetFlags(0)
7780

7881
vfsutil.ArchiveCacheDir = filepath.Join(*cacheDir, "lang-go-archive-cache")
82+
vfsutil.MaxCacheSizeBytes = *maxCacheSizeBytes
7983

8084
// Start pprof server, if desired.
8185
if *pprof != "" {
@@ -166,11 +170,12 @@ func run(cfg langserver.Config) error {
166170
connOpt = append(connOpt, jsonrpc2.LogMessages(log.New(logW, "", 0)))
167171
}
168172

169-
newHandler := func() jsonrpc2.Handler {
173+
newHandler := func() (jsonrpc2.Handler, io.Closer) {
170174
if *useBuildServer {
171-
return jsonrpc2.AsyncHandler(buildserver.NewHandler(cfg))
175+
handler := buildserver.NewHandler(cfg)
176+
return jsonrpc2.AsyncHandler(jsonrpc2.HandlerWithError(handler.Handle)), handler
172177
}
173-
return langserver.NewHandler(cfg)
178+
return langserver.NewHandler(cfg), ioutil.NopCloser(strings.NewReader(""))
174179
}
175180

176181
switch *mode {
@@ -194,9 +199,14 @@ func run(cfg langserver.Config) error {
194199
connectionID := connectionCount
195200
log.Printf("langserver-go: received incoming connection #%d\n", connectionID)
196201
openGauge.Inc()
197-
jsonrpc2Connection := jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(conn, jsonrpc2.VSCodeObjectCodec{}), newHandler(), connOpt...)
202+
handler, closer := newHandler()
203+
jsonrpc2Connection := jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(conn, jsonrpc2.VSCodeObjectCodec{}), handler, connOpt...)
198204
go func() {
199205
<-jsonrpc2Connection.DisconnectNotify()
206+
err := closer.Close()
207+
if err != nil {
208+
log.Println(err)
209+
}
200210
log.Printf("langserver-go: connection #%d closed\n", connectionID)
201211
openGauge.Dec()
202212
}()
@@ -221,7 +231,12 @@ func run(cfg langserver.Config) error {
221231

222232
openGauge.Inc()
223233
log.Printf("langserver-go: received incoming connection #%d\n", connectionID)
224-
<-jsonrpc2.NewConn(context.Background(), wsjsonrpc2.NewObjectStream(connection), newHandler(), connOpt...).DisconnectNotify()
234+
handler, closer := newHandler()
235+
<-jsonrpc2.NewConn(context.Background(), wsjsonrpc2.NewObjectStream(connection), handler, connOpt...).DisconnectNotify()
236+
err = closer.Close()
237+
if err != nil {
238+
log.Println(err)
239+
}
225240
log.Printf("langserver-go: connection #%d closed\n", connectionID)
226241
openGauge.Dec()
227242
})
@@ -243,7 +258,12 @@ func run(cfg langserver.Config) error {
243258

244259
case "stdio":
245260
log.Println("langserver-go: reading on stdin, writing on stdout")
246-
<-jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(stdrwc{}, jsonrpc2.VSCodeObjectCodec{}), newHandler(), connOpt...).DisconnectNotify()
261+
handler, closer := newHandler()
262+
<-jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(stdrwc{}, jsonrpc2.VSCodeObjectCodec{}), handler, connOpt...).DisconnectNotify()
263+
err := closer.Close()
264+
if err != nil {
265+
log.Println(err)
266+
}
247267
log.Println("connection closed")
248268
return nil
249269

0 commit comments

Comments
 (0)