Skip to content

Commit 0b1f1d4

Browse files
committed
gopls/internal/lsp/cache: (re-)ensure clean shutdown
CL 549415 (rightly) changed the logic for View shutdown to not await all work on the Snapshot, as this leads to potential deadlocks: we should never await work while holding a mutex. However, we still need to await all work when shutting down the Session, otherwise we end up with failures like golang/go#64971 ("directory not empty"). Therefore, we need a new synchronization mechanism. Introduce a sync.WaitGroup on the Session to allow awaiting the destruction of all Snapshots created on behalf of the Session. In order to support this, View invalidation becomes a method on the Session, rather than the View, and requires the Session.viewMu. Also make a few unrelated cosmetic improvements. Fixes golang/go#64971 Change-Id: I43fc0b5ff8a7762887fbfd64df7596e524383279 Reviewed-on: https://go-review.googlesource.com/c/tools/+/554996 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 706525d commit 0b1f1d4

File tree

4 files changed

+58
-40
lines changed

4 files changed

+58
-40
lines changed

gopls/internal/lsp/cache/session.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ type Session struct {
4242
views []*View
4343
viewMap map[protocol.DocumentURI]*View // file->best view; nil after shutdown
4444

45+
// snapshots is a counting semaphore that records the number
46+
// of unreleased snapshots associated with this session.
47+
// Shutdown waits for it to fall to zero.
48+
snapshotWG sync.WaitGroup
49+
4550
parseCache *parseCache
4651

4752
*overlayFS
@@ -68,6 +73,7 @@ func (s *Session) Shutdown(ctx context.Context) {
6873
view.shutdown()
6974
}
7075
s.parseCache.stop()
76+
s.snapshotWG.Wait() // wait for all work on associated snapshots to finish
7177
event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s))
7278
}
7379

@@ -183,12 +189,14 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
183189
},
184190
}
185191

192+
s.snapshotWG.Add(1)
186193
v.snapshot = &Snapshot{
187194
view: v,
188195
backgroundCtx: backgroundCtx,
189196
cancel: cancel,
190197
store: s.cache.store,
191198
refcount: 1, // Snapshots are born referenced.
199+
done: s.snapshotWG.Done,
192200
packages: new(persistent.Map[PackageID, *packageHandle]),
193201
meta: new(metadata.Graph),
194202
files: newFileMap(),
@@ -217,7 +225,7 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
217225

218226
// Initialize the view without blocking.
219227
initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx))
220-
v.initCancelFirstAttempt = initCancel
228+
v.cancelInitialWorkspaceLoad = initCancel
221229
snapshot := v.snapshot
222230

223231
// Pass a second reference to the background goroutine.
@@ -822,7 +830,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []file.Modificatio
822830
// ...but changes may be relevant to other views, for example if they are
823831
// changes to a shared package.
824832
for _, v := range s.views {
825-
_, release, needsDiagnosis := v.Invalidate(ctx, StateChange{Files: changed})
833+
_, release, needsDiagnosis := s.invalidateViewLocked(ctx, v, StateChange{Files: changed})
826834
release()
827835

828836
if needsDiagnosis || checkViews {

gopls/internal/lsp/cache/snapshot.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,14 @@ type Snapshot struct {
8585
store *memoize.Store // cache of handles shared by all snapshots
8686

8787
refMu sync.Mutex
88+
8889
// refcount holds the number of outstanding references to the current
89-
// Snapshot. When refcount is decremented to 0, the Snapshot maps can be
90-
// safely destroyed.
90+
// Snapshot. When refcount is decremented to 0, the Snapshot maps are
91+
// destroyed and the done function is called.
9192
//
9293
// TODO(rfindley): use atomic.Int32 on Go 1.19+.
9394
refcount int
95+
done func() // for implementing Session.Shutdown
9496

9597
// mu guards all of the maps in the snapshot, as well as the builtin URI and
9698
// initialized.
@@ -248,6 +250,7 @@ func (s *Snapshot) decref() {
248250
s.unloadableFiles.Destroy()
249251
s.moduleUpgrades.Destroy()
250252
s.vulns.Destroy()
253+
s.done()
251254
}
252255
}
253256

@@ -1663,10 +1666,10 @@ func inVendor(uri protocol.DocumentURI) bool {
16631666
// also require more strictness about diagnostic dependencies. For example,
16641667
// template.Diagnostics currently re-parses every time: there is no Snapshot
16651668
// data responsible for providing these diagnostics.
1666-
func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange) (*Snapshot, bool) {
1669+
func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done func()) (*Snapshot, bool) {
16671670
changedFiles := changed.Files
1668-
ctx, done := event.Start(ctx, "cache.snapshot.clone")
1669-
defer done()
1671+
ctx, stop := event.Start(ctx, "cache.snapshot.clone")
1672+
defer stop()
16701673

16711674
s.mu.Lock()
16721675
defer s.mu.Unlock()
@@ -1680,6 +1683,7 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange) (*Snap
16801683
sequenceID: s.sequenceID + 1,
16811684
store: s.store,
16821685
refcount: 1, // Snapshots are born referenced.
1686+
done: done,
16831687
view: s.view,
16841688
backgroundCtx: bgCtx,
16851689
cancel: cancel,

gopls/internal/lsp/cache/view.go

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"golang.org/x/tools/gopls/internal/settings"
3030
"golang.org/x/tools/gopls/internal/util/maps"
3131
"golang.org/x/tools/gopls/internal/util/pathutil"
32+
"golang.org/x/tools/gopls/internal/util/slices"
3233
"golang.org/x/tools/gopls/internal/vulncheck"
3334
"golang.org/x/tools/internal/event"
3435
"golang.org/x/tools/internal/gocommand"
@@ -100,21 +101,12 @@ type View struct {
100101
// ignoreFilter is used for fast checking of ignored files.
101102
ignoreFilter *ignoreFilter
102103

103-
// initCancelFirstAttempt can be used to terminate the view's first
104+
// cancelInitialWorkspaceLoad can be used to terminate the view's first
104105
// attempt at initialization.
105-
initCancelFirstAttempt context.CancelFunc
106+
cancelInitialWorkspaceLoad context.CancelFunc
106107

107-
// Track the latest snapshot via the snapshot field, guarded by snapshotMu.
108-
//
109-
// Invariant: whenever the snapshot field is overwritten, destroy(snapshot)
110-
// is called on the previous (overwritten) snapshot while snapshotMu is held,
111-
// incrementing snapshotWG. During shutdown the final snapshot is
112-
// overwritten with nil and destroyed, guaranteeing that all observed
113-
// snapshots have been destroyed via the destroy method, and snapshotWG may
114-
// be waited upon to let these destroy operations complete.
115108
snapshotMu sync.Mutex
116-
snapshot *Snapshot // latest snapshot; nil after shutdown has been called
117-
snapshotWG sync.WaitGroup // refcount for pending destroy operations
109+
snapshot *Snapshot // latest snapshot; nil after shutdown has been called
118110

119111
// initialWorkspaceLoad is closed when the first workspace initialization has
120112
// completed. If we failed to load, we only retry if the go.mod file changes,
@@ -513,11 +505,10 @@ func (v *View) filterFunc() func(protocol.DocumentURI) bool {
513505
}
514506
}
515507

516-
// shutdown releases resources associated with the view, and waits for ongoing
517-
// work to complete.
508+
// shutdown releases resources associated with the view.
518509
func (v *View) shutdown() {
519510
// Cancel the initial workspace load if it is still running.
520-
v.initCancelFirstAttempt()
511+
v.cancelInitialWorkspaceLoad()
521512

522513
v.snapshotMu.Lock()
523514
if v.snapshot != nil {
@@ -526,8 +517,6 @@ func (v *View) shutdown() {
526517
v.snapshot = nil
527518
}
528519
v.snapshotMu.Unlock()
529-
530-
v.snapshotWG.Wait()
531520
}
532521

533522
// IgnoredFile reports if a file would be ignored by a `go list` of the whole
@@ -767,16 +756,33 @@ type StateChange struct {
767756
GCDetails map[metadata.PackageID]bool // package -> whether or not we want details
768757
}
769758

770-
// Invalidate processes the provided state change, invalidating any derived
759+
// InvalidateView processes the provided state change, invalidating any derived
771760
// results that depend on the changed state.
772761
//
773762
// The resulting snapshot is non-nil, representing the outcome of the state
774763
// change. The second result is a function that must be called to release the
775764
// snapshot when the snapshot is no longer needed.
776765
//
777-
// The resulting bool reports whether the new View needs to be re-diagnosed.
778-
// See Snapshot.clone for more details.
779-
func (v *View) Invalidate(ctx context.Context, changed StateChange) (*Snapshot, func(), bool) {
766+
// An error is returned if the given view is no longer active in the session.
767+
func (s *Session) InvalidateView(ctx context.Context, view *View, changed StateChange) (*Snapshot, func(), error) {
768+
s.viewMu.Lock()
769+
defer s.viewMu.Unlock()
770+
771+
if !slices.Contains(s.views, view) {
772+
return nil, nil, fmt.Errorf("view is no longer active")
773+
}
774+
snapshot, release, _ := s.invalidateViewLocked(ctx, view, changed)
775+
return snapshot, release, nil
776+
}
777+
778+
// invalidateViewLocked invalidates the content of the given view.
779+
// (See [Session.InvalidateView]).
780+
//
781+
// The resulting bool reports whether the View needs to be re-diagnosed.
782+
// (See [Snapshot.clone]).
783+
//
784+
// s.viewMu must be held while calling this method.
785+
func (s *Session) invalidateViewLocked(ctx context.Context, v *View, changed StateChange) (*Snapshot, func(), bool) {
780786
// Detach the context so that content invalidation cannot be canceled.
781787
ctx = xcontext.Detach(ctx)
782788

@@ -799,9 +805,9 @@ func (v *View) Invalidate(ctx context.Context, changed StateChange) (*Snapshot,
799805
// TODO(rfindley): shouldn't we do this before canceling?
800806
prevSnapshot.AwaitInitialized(ctx)
801807

802-
// Save one lease of the cloned snapshot in the view.
803808
var needsDiagnosis bool
804-
v.snapshot, needsDiagnosis = prevSnapshot.clone(ctx, v.baseCtx, changed)
809+
s.snapshotWG.Add(1)
810+
v.snapshot, needsDiagnosis = prevSnapshot.clone(ctx, v.baseCtx, changed, s.snapshotWG.Done)
805811

806812
// Remove the initial reference created when prevSnapshot was created.
807813
prevSnapshot.decref()

gopls/internal/server/command.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -285,10 +285,9 @@ func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUp
285285
if err != nil {
286286
return nil, nil, err
287287
}
288-
snapshot, release, _ := deps.snapshot.View().Invalidate(ctx, cache.StateChange{
288+
return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
289289
ModuleUpgrades: map[protocol.DocumentURI]map[string]string{args.URI: upgrades},
290290
})
291-
return snapshot, release, nil
292291
})
293292
})
294293
}
@@ -306,15 +305,14 @@ func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command
306305
forURI: args.URI,
307306
}, func(ctx context.Context, deps commandDeps) error {
308307
return c.modifyState(ctx, FromResetGoModDiagnostics, func() (*cache.Snapshot, func(), error) {
309-
snapshot, release, _ := deps.snapshot.View().Invalidate(ctx, cache.StateChange{
308+
return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
310309
ModuleUpgrades: map[protocol.DocumentURI]map[string]string{
311310
deps.fh.URI(): nil,
312311
},
313312
Vulns: map[protocol.DocumentURI]*vulncheck.Result{
314313
deps.fh.URI(): nil,
315314
},
316315
})
317-
return snapshot, release, nil
318316
})
319317
})
320318
}
@@ -443,7 +441,7 @@ func (c *commandHandler) RemoveDependency(ctx context.Context, args command.Remo
443441
if err != nil {
444442
return err
445443
}
446-
edits, err := dropDependency(deps.snapshot, pm, args.ModulePath)
444+
edits, err := dropDependency(pm, args.ModulePath)
447445
if err != nil {
448446
return err
449447
}
@@ -476,7 +474,7 @@ func (c *commandHandler) RemoveDependency(ctx context.Context, args command.Remo
476474

477475
// dropDependency returns the edits to remove the given require from the go.mod
478476
// file.
479-
func dropDependency(snapshot *cache.Snapshot, pm *cache.ParsedModule, modulePath string) ([]protocol.TextEdit, error) {
477+
func dropDependency(pm *cache.ParsedModule, modulePath string) ([]protocol.TextEdit, error) {
480478
// We need a private copy of the parsed go.mod file, since we're going to
481479
// modify it.
482480
copied, err := modfile.Parse("", pm.Mapper.Content, nil)
@@ -796,12 +794,11 @@ func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIAr
796794
return nil, nil, err
797795
}
798796
wantDetails := !deps.snapshot.WantGCDetails(meta.ID) // toggle the gc details state
799-
snapshot, release, _ := deps.snapshot.View().Invalidate(ctx, cache.StateChange{
797+
return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
800798
GCDetails: map[metadata.PackageID]bool{
801799
meta.ID: wantDetails,
802800
},
803801
})
804-
return snapshot, release, nil
805802
})
806803
})
807804
}
@@ -995,9 +992,12 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch
995992
return err
996993
}
997994

998-
snapshot, release, _ := deps.snapshot.View().Invalidate(ctx, cache.StateChange{
995+
snapshot, release, err := c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{
999996
Vulns: map[protocol.DocumentURI]*vulncheck.Result{args.URI: result},
1000997
})
998+
if err != nil {
999+
return err
1000+
}
10011001
defer release()
10021002
c.s.diagnoseSnapshot(snapshot, nil, 0)
10031003

@@ -1292,7 +1292,7 @@ func (c *commandHandler) ChangeSignature(ctx context.Context, args command.Chang
12921292
func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.DiagnoseFilesArgs) error {
12931293
return c.run(ctx, commandConfig{
12941294
progress: "Diagnose files",
1295-
}, func(ctx context.Context, deps commandDeps) error {
1295+
}, func(ctx context.Context, _ commandDeps) error {
12961296

12971297
// TODO(rfindley): even better would be textDocument/diagnostics (golang/go#60122).
12981298
// Though note that implementing pull diagnostics may cause some servers to

0 commit comments

Comments
 (0)