Skip to content

Commit 79df971

Browse files
committed
gopls/internal/server: avoid duplicate diagnoses and loads
With [email protected], zero config gopls made it much more likely that sessions would have multiple Views. Additionally, with improved build tag support, it made it more likely that these Views would share files. As a result, we encountered (and fixed) this latent bug: 1. User changes file x.go, invalidating view A and B. A and B are scheduled for diagnosis. 2. User changes file y.go, invalidating only view B. Step (1) is cancelled and view B is scheduled for diagnosis. 3. View A never gets rediagnosed. The fix was naive: just mark view A and B as dirty, and schedule a goroutine to diagnose all dirty views after each step. As before, step (2) would cancel the context from step (1). But there's a problem: diagnoses were happening on the *Snapshot* context, not the operation context. Therefore, if the goroutines of step (1) and (2) both find the same snapshots, the diagnostics of step (1) would *not* be cancelled, and would be performed in addition to the diagnostics of (2). In other words, following a sequence of invalidations, we could theoretically be collecting diagnostics N times rather than 1 time. In practice, this is not so much of a problem for smaller repositories. Most of the time, changes are arriving at the speed of keystrokes, and diagnostics are being scheduled faster than we can type. However, on slower machines, or when there is more overall work being scheduled, or when changes arrive simultaneously (such as with a 'save all' command or branch switch), it is quite possible in practice to cause gopls to do more work than necessary, including redundant loads. I'm not sure if this is what conspires to cause the regressions described in golang/go#66647, but it certainly is a real regression. Fix this by threading the correct context into diagnoseSnapshot. Additionally, add earlier context cancellation in a few cases where redundant work was being performed despite a context cancellation. For golang/go#66647 Change-Id: I67da1c186848286ca7b6221330a655d23820fd5d Reviewed-on: https://go-review.googlesource.com/c/tools/+/577695 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent bcd607e commit 79df971

File tree

6 files changed

+40
-11
lines changed

6 files changed

+40
-11
lines changed

gopls/internal/cache/load.go

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ var errNoPackages = errors.New("no packages returned")
4343
//
4444
// If scopes contains a file scope there must be exactly one scope.
4545
func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) {
46+
if ctx.Err() != nil {
47+
// Check context cancellation before incrementing id below: a load on a
48+
// cancelled context should be a no-op.
49+
return ctx.Err()
50+
}
4651
id := atomic.AddUint64(&loadID, 1)
4752
eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging
4853

gopls/internal/cache/session.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -369,11 +369,11 @@ func (s *Session) SnapshotOf(ctx context.Context, uri protocol.DocumentURI) (*Sn
369369
if err != nil {
370370
continue // view was shut down
371371
}
372-
_ = snapshot.awaitLoaded(ctx) // ignore error
373-
g := snapshot.MetadataGraph()
374372
// We don't check the error from awaitLoaded, because a load failure (that
375373
// doesn't result from context cancelation) should not prevent us from
376374
// continuing to search for the best view.
375+
_ = snapshot.awaitLoaded(ctx)
376+
g := snapshot.MetadataGraph()
377377
if ctx.Err() != nil {
378378
release()
379379
return nil, nil, ctx.Err()
@@ -1121,6 +1121,11 @@ func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[protocol.Rel
11211121
//
11221122
// The caller must not mutate the result.
11231123
func (s *Session) OrphanedFileDiagnostics(ctx context.Context) (map[protocol.DocumentURI][]*Diagnostic, error) {
1124+
if err := ctx.Err(); err != nil {
1125+
// Avoid collecting diagnostics if the context is cancelled.
1126+
// (Previously, it was possible to get all the way to packages.Load on a cancelled context)
1127+
return nil, err
1128+
}
11241129
// Note: diagnostics holds a slice for consistency with other diagnostic
11251130
// funcs.
11261131
diagnostics := make(map[protocol.DocumentURI][]*Diagnostic)

gopls/internal/cache/snapshot.go

+4
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,10 @@ func (s *Snapshot) AwaitInitialized(ctx context.Context) {
13811381

13821382
// reloadWorkspace reloads the metadata for all invalidated workspace packages.
13831383
func (s *Snapshot) reloadWorkspace(ctx context.Context) {
1384+
if ctx.Err() != nil {
1385+
return
1386+
}
1387+
13841388
var scopes []loadScope
13851389
var seen map[PackagePath]bool
13861390
s.mu.Lock()

gopls/internal/server/command.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ func (c *commandHandler) modifyState(ctx context.Context, source ModificationSou
281281
}
282282
wg.Add(1)
283283
go func() {
284-
c.s.diagnoseSnapshot(snapshot, nil, 0)
284+
// Diagnosing with the background context ensures new snapshots are fully
285+
// diagnosed.
286+
c.s.diagnoseSnapshot(snapshot.BackgroundContext(), snapshot, nil, 0)
285287
release()
286288
wg.Done()
287289
}()
@@ -1076,7 +1078,10 @@ func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.Vulnch
10761078
return err
10771079
}
10781080
defer release()
1079-
c.s.diagnoseSnapshot(snapshot, nil, 0)
1081+
1082+
// Diagnosing with the background context ensures new snapshots are fully
1083+
// diagnosed.
1084+
c.s.diagnoseSnapshot(snapshot.BackgroundContext(), snapshot, nil, 0)
10801085

10811086
affecting := make(map[string]bool, len(result.Entries))
10821087
for _, finding := range result.Findings {
@@ -1408,7 +1413,11 @@ func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.Diagnos
14081413
wg.Add(1)
14091414
go func() {
14101415
defer wg.Done()
1411-
c.s.diagnoseSnapshot(snapshot, nil, 0)
1416+
1417+
// Use the operation context for diagnosis, rather than
1418+
// snapshot.BackgroundContext, because this operation does not create
1419+
// new snapshots (so they should also be diagnosed by other means).
1420+
c.s.diagnoseSnapshot(ctx, snapshot, nil, 0)
14121421
}()
14131422
}
14141423
wg.Wait()

gopls/internal/server/diagnostics.go

+11-5
Original file line numberDiff line numberDiff line change
@@ -135,16 +135,16 @@ func (s *server) diagnoseChangedViews(ctx context.Context, modID uint64, lastCha
135135
go func(snapshot *cache.Snapshot, uris []protocol.DocumentURI) {
136136
defer release()
137137
defer wg.Done()
138-
s.diagnoseSnapshot(snapshot, uris, snapshot.Options().DiagnosticsDelay)
138+
s.diagnoseSnapshot(ctx, snapshot, uris, snapshot.Options().DiagnosticsDelay)
139139
s.modificationMu.Lock()
140140

141-
// Only remove v from s.viewsToDiagnose if the snapshot is not cancelled.
141+
// Only remove v from s.viewsToDiagnose if the context is not cancelled.
142142
// This ensures that the snapshot was not cloned before its state was
143143
// fully evaluated, and therefore avoids missing a change that was
144144
// irrelevant to an incomplete snapshot.
145145
//
146146
// See the documentation for s.viewsToDiagnose for details.
147-
if snapshot.BackgroundContext().Err() == nil && s.viewsToDiagnose[v] <= modID {
147+
if ctx.Err() == nil && s.viewsToDiagnose[v] <= modID {
148148
delete(s.viewsToDiagnose, v)
149149
}
150150
s.modificationMu.Unlock()
@@ -173,8 +173,14 @@ func (s *server) diagnoseChangedViews(ctx context.Context, modID uint64, lastCha
173173
// If changedURIs is non-empty, it is a set of recently changed files that
174174
// should be diagnosed immediately, and onDisk reports whether these file
175175
// changes came from a change to on-disk files.
176-
func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protocol.DocumentURI, delay time.Duration) {
177-
ctx := snapshot.BackgroundContext()
176+
//
177+
// If the provided context is cancelled, diagnostics may be partially
178+
// published. Therefore, the provided context should only be cancelled if there
179+
// will be a subsequent operation to make diagnostics consistent. In general,
180+
// if an operation creates a new snapshot, it is responsible for ensuring that
181+
// snapshot (or a subsequent snapshot in the same View) is eventually
182+
// diagnosed.
183+
func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot, changedURIs []protocol.DocumentURI, delay time.Duration) {
178184
ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", snapshot.Labels()...)
179185
defer done()
180186

gopls/internal/server/general.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ func (s *server) addFolders(ctx context.Context, folders []protocol.WorkspaceFol
332332
// Diagnose the newly created view asynchronously.
333333
ndiagnose.Add(1)
334334
go func() {
335-
s.diagnoseSnapshot(snapshot, nil, 0)
335+
s.diagnoseSnapshot(snapshot.BackgroundContext(), snapshot, nil, 0)
336336
<-initialized
337337
release()
338338
ndiagnose.Done()

0 commit comments

Comments
 (0)