Skip to content

Commit 5eff1ee

Browse files
committed
gopls/internal/cache: check viewMap before altering views
The bug report in golang/go#67144 likely means that we got a change notification after the session was shut down (and thus s.viewMap was nil). Fix this by being more rigorous in guarding any function that resets s.viewMap with a check for s.viewMap != nil. Also, refactor to remove the confusing updateViewLocked and dropView functions, which obscure the logic of their callers. Fixes golang/go#67144 Change-Id: Ic76ae56fa631f6a7b11709437ad74a2897d1e537 Reviewed-on: https://go-review.googlesource.com/c/tools/+/589456 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent da9cad4 commit 5eff1ee

File tree

3 files changed

+89
-98
lines changed

3 files changed

+89
-98
lines changed

gopls/internal/cache/session.go

+81-90
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ type Session struct {
6363

6464
viewMu sync.Mutex
6565
views []*View
66-
viewMap map[protocol.DocumentURI]*View // file->best view; nil after shutdown
66+
viewMap map[protocol.DocumentURI]*View // file->best view or nil; nil after shutdown
6767

6868
// snapshots is a counting semaphore that records the number
6969
// of unreleased snapshots associated with this session.
@@ -117,6 +117,10 @@ func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, *Snapshot
117117
s.viewMu.Lock()
118118
defer s.viewMu.Unlock()
119119

120+
if s.viewMap == nil {
121+
return nil, nil, nil, fmt.Errorf("session is shut down")
122+
}
123+
120124
// Querying the file system to check whether
121125
// two folders denote the same existing directory.
122126
if inode1, err := os.Stat(filepath.FromSlash(folder.Dir.Path())); err == nil {
@@ -303,22 +307,30 @@ var (
303307

304308
// RemoveView removes from the session the view rooted at the specified directory.
305309
// It reports whether a view of that directory was removed.
306-
func (s *Session) RemoveView(dir protocol.DocumentURI) bool {
310+
func (s *Session) RemoveView(ctx context.Context, dir protocol.DocumentURI) bool {
307311
s.viewMu.Lock()
308312
defer s.viewMu.Unlock()
313+
314+
if s.viewMap == nil {
315+
return false // Session is shutdown.
316+
}
317+
s.viewMap = make(map[protocol.DocumentURI]*View) // reset view associations
318+
319+
var newViews []*View
309320
for _, view := range s.views {
310321
if view.folder.Dir == dir {
311-
i := s.dropView(view)
312-
if i == -1 {
313-
return false // can't happen
314-
}
315-
// delete this view... we don't care about order but we do want to make
316-
// sure we can garbage collect the view
317-
s.views = removeElement(s.views, i)
318-
return true
322+
view.shutdown()
323+
} else {
324+
newViews = append(newViews, view)
319325
}
320326
}
321-
return false
327+
removed := len(s.views) - len(newViews)
328+
if removed != 1 {
329+
// This isn't a bug report, because it could be a client-side bug.
330+
event.Error(ctx, "removing view", fmt.Errorf("removed %d views, want exactly 1", removed))
331+
}
332+
s.views = newViews
333+
return removed > 0
322334
}
323335

324336
// View returns the view with a matching id, if present.
@@ -434,30 +446,30 @@ var errNoViews = errors.New("no views")
434446
//
435447
// May return (nil, nil) if no best view can be determined.
436448
func (s *Session) viewOfLocked(ctx context.Context, uri protocol.DocumentURI) (*View, error) {
449+
if s.viewMap == nil {
450+
return nil, errors.New("session is shut down")
451+
}
437452
v, hit := s.viewMap[uri]
438453
if !hit {
439454
// Cache miss: compute (and memoize) the best view.
440455
fh, err := s.ReadFile(ctx, uri)
441456
if err != nil {
442457
return nil, err
443458
}
444-
bestViews, err := BestViews(ctx, s, fh.URI(), s.views)
459+
relevantViews, err := RelevantViews(ctx, s, fh.URI(), s.views)
445460
if err != nil {
446461
return nil, err
447462
}
448-
v = matchingView(fh, bestViews)
449-
if v == nil && len(bestViews) > 0 {
450-
// If we have candidate views, but none of them matched the file's build
463+
v = matchingView(fh, relevantViews)
464+
if v == nil && len(relevantViews) > 0 {
465+
// If we have relevant views, but none of them matched the file's build
451466
// constraints, then we are still better off using one of them here.
452467
// Otherwise, logic may fall back to an inferior view, which lacks
453468
// relevant module information, leading to misleading diagnostics.
454469
// (as in golang/go#60776).
455-
v = bestViews[0]
456-
}
457-
if s.viewMap == nil {
458-
return nil, errors.New("session is shut down")
470+
v = relevantViews[0]
459471
}
460-
s.viewMap[uri] = v
472+
s.viewMap[uri] = v // may be nil
461473
}
462474
return v, nil
463475
}
@@ -527,13 +539,13 @@ checkFiles:
527539
if err != nil {
528540
return nil, err
529541
}
530-
bestViews, err := BestViews(ctx, fs, fh.URI(), defs)
542+
relevantViews, err := RelevantViews(ctx, fs, fh.URI(), defs)
531543
if err != nil {
532544
// We should never call selectViewDefs with a cancellable context, so
533545
// this should never fail.
534546
return nil, bug.Errorf("failed to find best view for open file: %v", err)
535547
}
536-
def := matchingView(fh, bestViews)
548+
def := matchingView(fh, relevantViews)
537549
if def != nil {
538550
continue // file covered by an existing view
539551
}
@@ -564,10 +576,11 @@ checkFiles:
564576
// Views and viewDefinitions.
565577
type viewDefiner interface{ definition() *viewDefinition }
566578

567-
// BestViews returns the most relevant subset of views for a given uri.
568-
//
569-
// This may be used to filter diagnostics to the most relevant builds.
570-
func BestViews[V viewDefiner](ctx context.Context, fs file.Source, uri protocol.DocumentURI, views []V) ([]V, error) {
579+
// RelevantViews returns the views that may contain the given URI, or nil if
580+
// none exist. A view is "relevant" if, ignoring build constraints, it may have
581+
// a workspace package containing uri. Therefore, the definition of relevance
582+
// depends on the view type.
583+
func RelevantViews[V viewDefiner](ctx context.Context, fs file.Source, uri protocol.DocumentURI, views []V) ([]V, error) {
571584
if len(views) == 0 {
572585
return nil, nil // avoid the call to findRootPattern
573586
}
@@ -640,35 +653,35 @@ func BestViews[V viewDefiner](ctx context.Context, fs file.Source, uri protocol.
640653
//
641654
// We only consider one type of view, since the matching view created by
642655
// defineView should be of the best type.
643-
var bestViews []V
656+
var relevantViews []V
644657
switch {
645658
case len(workViews) > 0:
646-
bestViews = workViews
659+
relevantViews = workViews
647660
case len(modViews) > 0:
648-
bestViews = modViews
661+
relevantViews = modViews
649662
case len(gopathViews) > 0:
650-
bestViews = gopathViews
663+
relevantViews = gopathViews
651664
case len(goPackagesViews) > 0:
652-
bestViews = goPackagesViews
665+
relevantViews = goPackagesViews
653666
case len(adHocViews) > 0:
654-
bestViews = adHocViews
667+
relevantViews = adHocViews
655668
}
656669

657-
return bestViews, nil
670+
return relevantViews, nil
658671
}
659672

660-
// matchingView returns the View or viewDefinition out of bestViews that
673+
// matchingView returns the View or viewDefinition out of relevantViews that
661674
// matches the given file's build constraints, or nil if no match is found.
662675
//
663676
// Making this function generic is convenient so that we can avoid mapping view
664677
// definitions back to views inside Session.DidModifyFiles, where performance
665678
// matters. It is, however, not the cleanest application of generics.
666679
//
667680
// Note: keep this function in sync with defineView.
668-
func matchingView[V viewDefiner](fh file.Handle, bestViews []V) V {
681+
func matchingView[V viewDefiner](fh file.Handle, relevantViews []V) V {
669682
var zero V
670683

671-
if len(bestViews) == 0 {
684+
if len(relevantViews) == 0 {
672685
return zero
673686
}
674687

@@ -678,14 +691,14 @@ func matchingView[V viewDefiner](fh file.Handle, bestViews []V) V {
678691
// Note that the behavior here on non-existent files shouldn't matter much,
679692
// since there will be a subsequent failure.
680693
if fileKind(fh) != file.Go || err != nil {
681-
return bestViews[0]
694+
return relevantViews[0]
682695
}
683696

684697
// Find the first view that matches constraints.
685698
// Content trimming is nontrivial, so do this outside of the loop below.
686699
path := fh.URI().Path()
687700
content = trimContentForPortMatch(content)
688-
for _, v := range bestViews {
701+
for _, v := range relevantViews {
689702
def := v.definition()
690703
viewPort := port{def.GOOS(), def.GOARCH()}
691704
if viewPort.matches(path, content) {
@@ -696,63 +709,35 @@ func matchingView[V viewDefiner](fh file.Handle, bestViews []V) V {
696709
return zero // no view found
697710
}
698711

699-
// updateViewLocked recreates the view with the given options.
700-
//
701-
// If the resulting error is non-nil, the view may or may not have already been
702-
// dropped from the session.
703-
func (s *Session) updateViewLocked(ctx context.Context, view *View, def *viewDefinition) (*View, error) {
704-
i := s.dropView(view)
705-
if i == -1 {
706-
return nil, fmt.Errorf("view %q not found", view.id)
707-
}
708-
709-
view, _, release := s.createView(ctx, def)
710-
defer release()
712+
// ResetView resets the best view for the given URI.
713+
func (s *Session) ResetView(ctx context.Context, uri protocol.DocumentURI) (*View, error) {
714+
s.viewMu.Lock()
715+
defer s.viewMu.Unlock()
711716

712-
// substitute the new view into the array where the old view was
713-
s.views[i] = view
714-
s.viewMap = make(map[protocol.DocumentURI]*View)
715-
return view, nil
716-
}
717+
if s.viewMap == nil {
718+
return nil, fmt.Errorf("session is shut down")
719+
}
717720

718-
// removeElement removes the ith element from the slice replacing it with the last element.
719-
// TODO(adonovan): generics, someday.
720-
func removeElement(slice []*View, index int) []*View {
721-
last := len(slice) - 1
722-
slice[index] = slice[last]
723-
slice[last] = nil // aid GC
724-
return slice[:last]
725-
}
721+
view, err := s.viewOfLocked(ctx, uri)
722+
if err != nil {
723+
return nil, err
724+
}
725+
if view == nil {
726+
return nil, fmt.Errorf("no view for %s", uri)
727+
}
726728

727-
// dropView removes v from the set of views for the receiver s and calls
728-
// v.shutdown, returning the index of v in s.views (if found), or -1 if v was
729-
// not found. s.viewMu must be held while calling this function.
730-
func (s *Session) dropView(v *View) int {
731-
// we always need to drop the view map
732729
s.viewMap = make(map[protocol.DocumentURI]*View)
733-
for i := range s.views {
734-
if v == s.views[i] {
735-
// we found the view, drop it and return the index it was found at
736-
s.views[i] = nil
730+
for i, v := range s.views {
731+
if v == view {
732+
v2, _, release := s.createView(ctx, view.viewDefinition)
733+
release() // don't need the snapshot
737734
v.shutdown()
738-
return i
735+
s.views[i] = v2
736+
return v2, nil
739737
}
740738
}
741-
// TODO(rfindley): it looks wrong that we don't shutdown v in this codepath.
742-
// We should never get here.
743-
bug.Reportf("tried to drop nonexistent view %q", v.id)
744-
return -1
745-
}
746739

747-
// ResetView resets the best view for the given URI.
748-
func (s *Session) ResetView(ctx context.Context, uri protocol.DocumentURI) (*View, error) {
749-
s.viewMu.Lock()
750-
defer s.viewMu.Unlock()
751-
v, err := s.viewOfLocked(ctx, uri)
752-
if err != nil {
753-
return nil, err
754-
}
755-
return s.updateViewLocked(ctx, v, v.viewDefinition)
740+
return nil, bug.Errorf("missing view") // can't happen...
756741
}
757742

758743
// DidModifyFiles reports a file modification to the session. It returns
@@ -768,6 +753,11 @@ func (s *Session) DidModifyFiles(ctx context.Context, modifications []file.Modif
768753
s.viewMu.Lock()
769754
defer s.viewMu.Unlock()
770755

756+
// Short circuit the logic below if s is shut down.
757+
if s.viewMap == nil {
758+
return nil, fmt.Errorf("session is shut down")
759+
}
760+
771761
// Update overlays.
772762
//
773763
// This is done while holding viewMu because the set of open files affects
@@ -899,9 +889,10 @@ func (s *Session) DidModifyFiles(ctx context.Context, modifications []file.Modif
899889
for _, mod := range modifications {
900890
v, err := s.viewOfLocked(ctx, mod.URI)
901891
if err != nil {
902-
// bestViewForURI only returns an error in the event of context
903-
// cancellation. Since state changes should occur on an uncancellable
904-
// context, an error here is a bug.
892+
// viewOfLocked only returns an error in the event of context
893+
// cancellation, or if the session is shut down. Since state changes
894+
// should occur on an uncancellable context, and s.viewMap was checked at
895+
// the top of this function, an error here is a bug.
905896
bug.Reportf("finding best view for change: %v", err)
906897
continue
907898
}

gopls/internal/server/diagnostics.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -853,30 +853,30 @@ func (s *server) publishFileDiagnosticsLocked(ctx context.Context, views viewSet
853853
allViews = append(allViews, view)
854854
}
855855

856-
// Only report diagnostics from the best views for a file. This avoids
856+
// Only report diagnostics from relevant views for a file. This avoids
857857
// spurious import errors when a view has only a partial set of dependencies
858858
// for a package (golang/go#66425).
859859
//
860860
// It's ok to use the session to derive the eligible views, because we
861-
// publish diagnostics following any state change, so the set of best views
862-
// is eventually consistent.
863-
bestViews, err := cache.BestViews(ctx, s.session, uri, allViews)
861+
// publish diagnostics following any state change, so the set of relevant
862+
// views is eventually consistent.
863+
relevantViews, err := cache.RelevantViews(ctx, s.session, uri, allViews)
864864
if err != nil {
865865
return err
866866
}
867867

868-
if len(bestViews) == 0 {
868+
if len(relevantViews) == 0 {
869869
// If we have no preferred diagnostics for a given file (i.e., the file is
870870
// not naturally nested within a view), then all diagnostics should be
871871
// considered valid.
872872
//
873873
// This could arise if the user jumps to definition outside the workspace.
874874
// There is no view that owns the file, so its diagnostics are valid from
875875
// any view.
876-
bestViews = allViews
876+
relevantViews = allViews
877877
}
878878

879-
for _, view := range bestViews {
879+
for _, view := range relevantViews {
880880
viewDiags := f.byView[view]
881881
// Compute the view's suffix (e.g. " [darwin,arm64]").
882882
var suffix string

gopls/internal/server/workspace.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func (s *server) DidChangeWorkspaceFolders(ctx context.Context, params *protocol
2929
if err != nil {
3030
return fmt.Errorf("invalid folder %q: %v", folder.URI, err)
3131
}
32-
if !s.session.RemoveView(dir) {
32+
if !s.session.RemoveView(ctx, dir) {
3333
return fmt.Errorf("view %q for %v not found", folder.Name, folder.URI)
3434
}
3535
}

0 commit comments

Comments
 (0)