gopls/internal/lsp: remove access to mutable session state from the view
To avoid concurrency bugs and/or operation on inconsistent session
state, remove visibility into the session from views. In a few places,
this refactoring highlighted bugs (such as incorrect usage of
view.SetOptions).
This broke one of the debug templates, for which it was simply easier to
remove the back-link from view to session.
Change-Id: I4dbce0dcebab6f25a9c70524310ae1e0e04e2d97
Reviewed-on: https://go-review.googlesource.com/c/tools/+/453815
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go
index 0eb00f2..8c06b6d 100644
--- a/gopls/internal/lsp/cache/cache.go
+++ b/gopls/internal/lsp/cache/cache.go
@@ -173,11 +173,11 @@
c.options(options)
}
s := &Session{
- cache: c,
id: strconv.FormatInt(index, 10),
+ cache: c,
+ gocmdRunner: &gocommand.Runner{},
options: options,
overlays: make(map[span.URI]*overlay),
- gocmdRunner: &gocommand.Runner{},
}
event.Log(ctx, "New session", KeyCreateSession.Of(s))
return s
diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go
index 321ff5f..ccba4e8 100644
--- a/gopls/internal/lsp/cache/load.go
+++ b/gopls/internal/lsp/cache/load.go
@@ -105,11 +105,6 @@
}
sort.Strings(query) // for determinism
- if s.view.Options().VerboseWorkDoneProgress {
- work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil)
- defer work.End(ctx, "Done.")
- }
-
ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query))
defer done()
diff --git a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go
index 97bdb2f..757bb5e 100644
--- a/gopls/internal/lsp/cache/mod.go
+++ b/gopls/internal/lsp/cache/mod.go
@@ -186,7 +186,7 @@
var sumFH source.FileHandle = s.FindFile(sumURI)
if sumFH == nil {
var err error
- sumFH, err = s.view.session.cache.getFile(ctx, sumURI)
+ sumFH, err = s.view.cache.getFile(ctx, sumURI)
if err != nil {
return nil
}
diff --git a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go
index fc97ee6..f433211 100644
--- a/gopls/internal/lsp/cache/mod_tidy.go
+++ b/gopls/internal/lsp/cache/mod_tidy.go
@@ -106,7 +106,7 @@
// Keep the temporary go.mod file around long enough to parse it.
defer cleanup()
- if _, err := snapshot.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
+ if _, err := snapshot.view.gocmdRunner.Run(ctx, *inv); err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go
index 28acd5d..5eca7c8 100644
--- a/gopls/internal/lsp/cache/session.go
+++ b/gopls/internal/lsp/cache/session.go
@@ -14,9 +14,9 @@
"sync/atomic"
"golang.org/x/tools/gopls/internal/govulncheck"
- "golang.org/x/tools/gopls/internal/lsp/progress"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/span"
+ "golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/imports"
@@ -25,8 +25,12 @@
)
type Session struct {
- cache *Cache
- id string
+ // Unique identifier for this session.
+ id string
+
+ // Immutable attributes shared across views.
+ cache *Cache // shared cache
+ gocmdRunner *gocommand.Runner // limits go command concurrency
optionsMu sync.Mutex
options *source.Options
@@ -37,11 +41,6 @@
overlayMu sync.Mutex
overlays map[span.URI]*overlay
-
- // gocmdRunner guards go command calls from concurrency errors.
- gocmdRunner *gocommand.Runner
-
- progress *progress.Tracker
}
type overlay struct {
@@ -139,12 +138,6 @@
s.options = options
}
-// SetProgressTracker sets the progress tracker for the session.
-func (s *Session) SetProgressTracker(tracker *progress.Tracker) {
- // The progress tracker should be set before any view is initialized.
- s.progress = tracker
-}
-
// Shutdown the session and all views it has created.
func (s *Session) Shutdown(ctx context.Context) {
var views []*View
@@ -154,7 +147,7 @@
s.viewMap = nil
s.viewMu.Unlock()
for _, view := range views {
- view.shutdown(ctx)
+ view.shutdown()
}
event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s))
}
@@ -169,7 +162,7 @@
// of its gopls workspace module in that directory, so that client tooling
// can execute in the same main module. On success it also returns a release
// function that must be called when the Snapshot is no longer needed.
-func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (source.View, source.Snapshot, func(), error) {
+func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (*View, source.Snapshot, func(), error) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
for _, view := range s.views {
@@ -230,10 +223,11 @@
backgroundCtx, cancel := context.WithCancel(baseCtx)
v := &View{
- session: s,
+ id: strconv.FormatInt(index, 10),
+ cache: s.cache,
+ gocmdRunner: s.gocmdRunner,
initialWorkspaceLoad: make(chan struct{}),
initializationSema: make(chan struct{}, 1),
- id: strconv.FormatInt(index, 10),
options: options,
baseCtx: baseCtx,
name: name,
@@ -308,7 +302,7 @@
}
// View returns a view with a matching name, if the session has one.
-func (s *Session) View(name string) source.View {
+func (s *Session) View(name string) *View {
s.viewMu.RLock()
defer s.viewMu.RUnlock()
for _, view := range s.views {
@@ -321,11 +315,7 @@
// ViewOf returns a view corresponding to the given URI.
// If the file is not already associated with a view, pick one using some heuristics.
-func (s *Session) ViewOf(uri span.URI) (source.View, error) {
- return s.viewOf(uri)
-}
-
-func (s *Session) viewOf(uri span.URI) (*View, error) {
+func (s *Session) ViewOf(uri span.URI) (*View, error) {
s.viewMu.RLock()
defer s.viewMu.RUnlock()
// Check if we already know this file.
@@ -340,13 +330,11 @@
return s.viewMap[uri], nil
}
-func (s *Session) Views() []source.View {
+func (s *Session) Views() []*View {
s.viewMu.RLock()
defer s.viewMu.RUnlock()
- result := make([]source.View, len(s.views))
- for i, v := range s.views {
- result[i] = v
- }
+ result := make([]*View, len(s.views))
+ copy(result, s.views)
return result
}
@@ -378,19 +366,19 @@
return views[0]
}
-func (s *Session) removeView(ctx context.Context, view *View) error {
+// RemoveView removes the view v from the session
+func (s *Session) RemoveView(view *View) {
s.viewMu.Lock()
defer s.viewMu.Unlock()
- i, err := s.dropView(ctx, view)
- if err != nil {
- return err
+ i := s.dropView(view)
+ if i == -1 { // error reported elsewhere
+ return
}
// delete this view... we don't care about order but we do want to make
// sure we can garbage collect the view
s.views[i] = s.views[len(s.views)-1]
s.views[len(s.views)-1] = nil
s.views = s.views[:len(s.views)-1]
- return nil
}
func (s *Session) updateView(ctx context.Context, view *View, options *source.Options) (*View, error) {
@@ -406,9 +394,9 @@
seqID := view.snapshot.sequenceID // Preserve sequence IDs when updating a view in place.
view.snapshotMu.Unlock()
- i, err := s.dropView(ctx, view)
- if err != nil {
- return nil, err
+ i := s.dropView(view)
+ if i == -1 {
+ return nil, fmt.Errorf("view %q not found", view.id)
}
v, _, release, err := s.createView(ctx, view.name, view.folder, options, seqID)
@@ -428,18 +416,24 @@
return v, nil
}
-func (s *Session) dropView(ctx context.Context, v *View) (int, error) {
+// dropView removes v from the set of views for the receiver s and calls
+// v.shutdown, returning the index of v in s.views (if found), or -1 if v was
+// not found. s.viewMu must be held while calling this function.
+func (s *Session) dropView(v *View) int {
// we always need to drop the view map
s.viewMap = make(map[span.URI]*View)
for i := range s.views {
if v == s.views[i] {
// we found the view, drop it and return the index it was found at
s.views[i] = nil
- v.shutdown(ctx)
- return i, nil
+ v.shutdown()
+ return i
}
}
- return -1, fmt.Errorf("view %s for %v not found", v.Name(), v.Folder())
+ // TODO(rfindley): it looks wrong that we don't shutdown v in this codepath.
+ // We should never get here.
+ bug.Reportf("tried to drop nonexistent view %q", v.id)
+ return -1
}
func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModification) error {
@@ -498,7 +492,7 @@
if c.OnDisk {
continue
}
- bestView, err := s.viewOf(c.URI)
+ bestView, err := s.ViewOf(c.URI)
if err != nil {
return nil, nil, err
}
diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go
index 1f738c6..5a15641 100644
--- a/gopls/internal/lsp/cache/snapshot.go
+++ b/gopls/internal/lsp/cache/snapshot.go
@@ -258,7 +258,7 @@
}
func (s *snapshot) FileSet() *token.FileSet {
- return s.view.session.cache.fset
+ return s.view.cache.fset
}
func (s *snapshot) ModFiles() []span.URI {
@@ -376,7 +376,7 @@
if typesinternal.SetUsesCgo(&types.Config{}) {
cfg.Mode |= packages.LoadMode(packagesinternal.TypecheckCgo)
}
- packagesinternal.SetGoCmdRunner(cfg, s.view.session.gocmdRunner)
+ packagesinternal.SetGoCmdRunner(cfg, s.view.gocmdRunner)
return cfg
}
@@ -387,7 +387,7 @@
}
defer cleanup()
- return s.view.session.gocmdRunner.Run(ctx, *inv)
+ return s.view.gocmdRunner.Run(ctx, *inv)
}
func (s *snapshot) RunGoCommandPiped(ctx context.Context, mode source.InvocationFlags, inv *gocommand.Invocation, stdout, stderr io.Writer) error {
@@ -396,7 +396,7 @@
return err
}
defer cleanup()
- return s.view.session.gocmdRunner.RunPiped(ctx, *inv, stdout, stderr)
+ return s.view.gocmdRunner.RunPiped(ctx, *inv, stdout, stderr)
}
func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd string, run func(invoke func(...string) (*bytes.Buffer, error)) error) (bool, []byte, []byte, error) {
@@ -417,7 +417,7 @@
invoke := func(args ...string) (*bytes.Buffer, error) {
inv.Verb = args[0]
inv.Args = args[1:]
- return s.view.session.gocmdRunner.Run(ctx, *inv)
+ return s.view.gocmdRunner.Run(ctx, *inv)
}
if err := run(invoke); err != nil {
return false, nil, nil, err
@@ -1319,7 +1319,7 @@
return fh, nil
}
- fh, err := s.view.session.cache.getFile(ctx, f.URI()) // read the file
+ fh, err := s.view.cache.getFile(ctx, f.URI()) // read the file
if err != nil {
return nil, err
}
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index db7cbb7..4adcfb1 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -36,8 +36,10 @@
)
type View struct {
- session *Session
- id string
+ id string
+
+ cache *Cache // shared cache
+ gocmdRunner *gocommand.Runner // limits go command concurrency
optionsMu sync.Mutex
options *source.Options
@@ -343,7 +345,11 @@
return reflect.DeepEqual(aBuildFlags, bBuildFlags)
}
-func (v *View) SetOptions(ctx context.Context, options *source.Options) (source.View, error) {
+// SetViewOptions sets the options of the given view to new values. Calling
+// this may cause the view to be invalidated and a replacement view added to
+// the session. If so the new view will be returned, otherwise the original one
+// will be returned.
+func (s *Session) SetViewOptions(ctx context.Context, v *View, options *source.Options) (*View, error) {
// no need to rebuild the view if the options were not materially changed
v.optionsMu.Lock()
if minorOptionsChange(v.options, options) {
@@ -352,7 +358,7 @@
return v, nil
}
v.optionsMu.Unlock()
- newView, err := v.session.updateView(ctx, v, options)
+ newView, err := s.updateView(ctx, v, options)
return newView, err
}
@@ -375,7 +381,7 @@
fullEnv[s[0]] = s[1]
}
}
- goVersion, err := s.view.session.gocmdRunner.Run(ctx, gocommand.Invocation{
+ goVersion, err := s.view.gocmdRunner.Run(ctx, gocommand.Invocation{
Verb: "version",
Env: env,
WorkingDir: s.view.rootURI.Filename(),
@@ -584,16 +590,9 @@
return nil, nil
}
-func (v *View) Shutdown(ctx context.Context) {
- v.session.removeView(ctx, v)
-}
-
// shutdown releases resources associated with the view, and waits for ongoing
// work to complete.
-//
-// TODO(rFindley): probably some of this should also be one in View.Shutdown
-// above?
-func (v *View) shutdown(ctx context.Context) {
+func (v *View) shutdown() {
// Cancel the initial workspace load if it is still running.
v.initCancelFirstAttempt()
@@ -610,10 +609,6 @@
v.snapshotWG.Wait()
}
-func (v *View) Session() *Session {
- return v.session
-}
-
func (s *snapshot) IgnoredFile(uri span.URI) bool {
filename := uri.Filename()
var prefixes []string
diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go
index 23d69ed..2257846 100644
--- a/gopls/internal/lsp/completion_test.go
+++ b/gopls/internal/lsp/completion_test.go
@@ -129,12 +129,12 @@
original := view.Options()
modified := view.Options().Clone()
options(modified)
- view, err = view.SetOptions(r.ctx, modified)
+ view, err = r.server.session.SetViewOptions(r.ctx, view, modified)
if err != nil {
t.Error(err)
return nil
}
- defer view.SetOptions(r.ctx, original)
+ defer r.server.session.SetViewOptions(r.ctx, view, original)
list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
diff --git a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go
index 248041e..abc939e 100644
--- a/gopls/internal/lsp/debug/serve.go
+++ b/gopls/internal/lsp/debug/serve.go
@@ -130,11 +130,7 @@
func (st *State) Views() []*cache.View {
var views []*cache.View
for _, s := range st.Sessions() {
- for _, v := range s.Views() {
- if cv, ok := v.(*cache.View); ok {
- views = append(views, cv)
- }
- }
+ views = append(views, s.Views()...)
}
return views
}
@@ -897,7 +893,6 @@
{{define "body"}}
Name: <b>{{.Name}}</b><br>
Folder: <b>{{.Folder}}</b><br>
-From: <b>{{template "sessionlink" .Session.ID}}</b><br>
<h2>Environment</h2>
<ul>{{range .Options.Env}}<li>{{.}}</li>{{end}}</ul>
{{end}}
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index d966d94..5a29cd8 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -70,12 +70,12 @@
t.Fatal(err)
}
- defer view.Shutdown(ctx)
+ defer session.RemoveView(view)
// Enable type error analyses for tests.
// TODO(golang/go#38212): Delete this once they are enabled by default.
tests.EnableAllAnalyzers(view, options)
- view.SetOptions(ctx, options)
+ session.SetViewOptions(ctx, view, options)
// Enable all inlay hints for tests.
tests.EnableAllInlayHints(view, options)
@@ -233,10 +233,11 @@
}
original := view.Options()
modified := original
+ defer r.server.session.SetViewOptions(r.ctx, view, original)
// Test all folding ranges.
modified.LineFoldingOnly = false
- view, err = view.SetOptions(r.ctx, modified)
+ view, err = r.server.session.SetViewOptions(r.ctx, view, modified)
if err != nil {
t.Error(err)
return
@@ -254,7 +255,7 @@
// Test folding ranges with lineFoldingOnly = true.
modified.LineFoldingOnly = true
- view, err = view.SetOptions(r.ctx, modified)
+ view, err = r.server.session.SetViewOptions(r.ctx, view, modified)
if err != nil {
t.Error(err)
return
@@ -269,7 +270,6 @@
return
}
r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges)
- view.SetOptions(r.ctx, original)
}
func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) {
@@ -1328,7 +1328,7 @@
}
}
-func (r *runner) collectDiagnostics(view source.View) {
+func (r *runner) collectDiagnostics(view *cache.View) {
if r.diagnostics != nil {
return
}
diff --git a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go
index 9f70591..6d1775f 100644
--- a/gopls/internal/lsp/server.go
+++ b/gopls/internal/lsp/server.go
@@ -23,8 +23,6 @@
// NewServer creates an LSP server and binds it to handle incoming client
// messages on on the supplied stream.
func NewServer(session *cache.Session, client protocol.ClientCloser) *Server {
- tracker := progress.NewTracker(client)
- session.SetProgressTracker(tracker)
return &Server{
diagnostics: map[span.URI]*fileReports{},
gcOptimizationDetails: make(map[source.PackageID]struct{}),
@@ -33,7 +31,7 @@
session: session,
client: client,
diagnosticsSema: make(chan struct{}, concurrentAnalyses),
- progress: tracker,
+ progress: progress.NewTracker(client),
diagDebouncer: newDebouncer(),
watchedFileDebouncer: newDebouncer(),
}
diff --git a/gopls/internal/lsp/source/source_test.go b/gopls/internal/lsp/source/source_test.go
index d7dc77c..e540530 100644
--- a/gopls/internal/lsp/source/source_test.go
+++ b/gopls/internal/lsp/source/source_test.go
@@ -38,8 +38,9 @@
}
type runner struct {
+ session *cache.Session
+ view *cache.View
snapshot source.Snapshot
- view source.View
data *tests.Data
ctx context.Context
normalizers []tests.Normalizer
@@ -58,12 +59,15 @@
t.Fatal(err)
}
release()
- defer view.Shutdown(ctx)
+ defer session.RemoveView(view)
// Enable type error analyses for tests.
// TODO(golang/go#38212): Delete this once they are enabled by default.
tests.EnableAllAnalyzers(view, options)
- view.SetOptions(ctx, options)
+ view, err = session.SetViewOptions(ctx, view, options)
+ if err != nil {
+ t.Fatal(err)
+ }
var modifications []source.FileModification
for filename, content := range datum.Config.Overlay {
@@ -84,6 +88,7 @@
snapshot, release := view.Snapshot(ctx)
defer release()
r := &runner{
+ session: session,
view: view,
snapshot: snapshot,
data: datum,
@@ -283,14 +288,14 @@
original := r.view.Options()
modified := original.Clone()
options(modified)
- newView, err := r.view.SetOptions(r.ctx, modified)
- if newView != r.view {
+ view, err := r.session.SetViewOptions(r.ctx, r.view, modified)
+ if view != r.view {
t.Fatalf("options change unexpectedly created new view")
}
if err != nil {
t.Fatal(err)
}
- defer r.view.SetOptions(r.ctx, original)
+ defer r.session.SetViewOptions(r.ctx, view, original)
list, surrounding, err := completion.Completion(r.ctx, r.snapshot, fh, protocol.Position{
Line: uint32(src.Start().Line() - 1),
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index 0e9dcd2..fa55cde 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -283,18 +283,9 @@
// Folder returns the folder with which this view was created.
Folder() span.URI
- // Shutdown closes this view, and detaches it from its session.
- Shutdown(ctx context.Context)
-
// Options returns a copy of the Options for this view.
Options() *Options
- // SetOptions sets the options of this view to new values.
- // Calling this may cause the view to be invalidated and a replacement view
- // added to the session. If so the new view will be returned, otherwise the
- // original one will be.
- SetOptions(context.Context, *Options) (View, error)
-
// Snapshot returns the current snapshot for the view, and a
// release function that must be called when the Snapshot is
// no longer needed.
diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go
index 4f9948e..e4326b1 100644
--- a/gopls/internal/lsp/workspace.go
+++ b/gopls/internal/lsp/workspace.go
@@ -18,7 +18,7 @@
for _, folder := range event.Removed {
view := s.session.View(folder.Name)
if view != nil {
- view.Shutdown(ctx)
+ s.session.RemoveView(view)
} else {
return fmt.Errorf("view %s for %v not found", folder.Name, folder.URI)
}
@@ -58,7 +58,7 @@
if err := s.fetchConfig(ctx, view.Name(), view.Folder(), options); err != nil {
return err
}
- view, err := view.SetOptions(ctx, options)
+ view, err := s.session.SetViewOptions(ctx, view, options)
if err != nil {
return err
}
diff --git a/gopls/internal/lsp/workspace_symbol.go b/gopls/internal/lsp/workspace_symbol.go
index 9101a3e..88b3e88 100644
--- a/gopls/internal/lsp/workspace_symbol.go
+++ b/gopls/internal/lsp/workspace_symbol.go
@@ -7,9 +7,9 @@
import (
"context"
- "golang.org/x/tools/internal/event"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/internal/event"
)
func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) {
@@ -19,5 +19,14 @@
views := s.session.Views()
matcher := s.session.Options().SymbolMatcher
style := s.session.Options().SymbolStyle
- return source.WorkspaceSymbols(ctx, matcher, style, views, params.Query)
+ // TODO(rfindley): it looks wrong that we need to pass views here.
+ //
+ // Evidence:
+ // - this is the only place we convert views to []source.View
+ // - workspace symbols is the only place where we call source.View.Snapshot
+ var sourceViews []source.View
+ for _, v := range views {
+ sourceViews = append(sourceViews, v)
+ }
+ return source.WorkspaceSymbols(ctx, matcher, style, sourceViews, params.Query)
}
diff --git a/gopls/internal/vulncheck/command_test.go b/gopls/internal/vulncheck/command_test.go
index b7a270e..4032693 100644
--- a/gopls/internal/vulncheck/command_test.go
+++ b/gopls/internal/vulncheck/command_test.go
@@ -319,7 +319,7 @@
// The snapshot must be released before calling view.Shutdown, to avoid a
// deadlock.
release()
- view.Shutdown(ctx)
+ session.RemoveView(view)
}()
test(ctx, snapshot)