Skip to content

Commit 35549c6

Browse files
committed
gopls/internal/lsp/cache: move upgrades and vulns onto the snapshot
To simplify the View and eliminate races, treat module upgrades and vulnerabilities like any other state by tracking them in the snapshot. With this change, and an additional change to track diagnostics done on behalf of ExecuteCommand, we can make TestUpgradeCodelens properly assertive and fix the underlying race of golang/go#58750. Also rewrite a unit test checking vuln result expiry as a regtest. Updates golang/go#57979 Fixes golang/go#58750 Change-Id: I4521c97f798eecd13844278a304070f661538382 Reviewed-on: https://go-review.googlesource.com/c/tools/+/540479 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 9fd7ea0 commit 35549c6

File tree

14 files changed

+243
-260
lines changed

14 files changed

+243
-260
lines changed

gopls/internal/immutable/immutable.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ func MapOf[K comparable, V any](m map[K]V) Map[K, V] {
2020
return Map[K, V]{m}
2121
}
2222

23+
// Keys returns all keys present in the map.
24+
func (m Map[K, V]) Keys() []K {
25+
var keys []K
26+
for k := range m.m {
27+
keys = append(keys, k)
28+
}
29+
return keys
30+
}
31+
2332
// Value returns the mapped value for k.
2433
// It is equivalent to the commaok form of an ordinary go map, and returns
2534
// (zero, false) if the key is not present.

gopls/internal/lsp/cache/session.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,6 @@ func (s *Session) createView(ctx context.Context, info *workspaceInformation, fo
128128
initialWorkspaceLoad: make(chan struct{}),
129129
initializationSema: make(chan struct{}, 1),
130130
baseCtx: baseCtx,
131-
moduleUpgrades: map[span.URI]map[string]string{},
132-
vulns: map[span.URI]*vulncheck.Result{},
133131
parseCache: s.parseCache,
134132
fs: s.overlayFS,
135133
workspaceInformation: info,
@@ -173,6 +171,8 @@ func (s *Session) createView(ctx context.Context, info *workspaceInformation, fo
173171
workspaceModFiles: wsModFiles,
174172
workspaceModFilesErr: wsModFilesErr,
175173
pkgIndex: typerefs.NewPackageIndex(),
174+
moduleUpgrades: new(persistent.Map[span.URI, map[string]string]),
175+
vulns: new(persistent.Map[span.URI, *vulncheck.Result]),
176176
}
177177
// Save one reference in the view.
178178
v.releaseSnapshot = v.snapshot.Acquire()
@@ -501,9 +501,9 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
501501
}
502502

503503
var releases []func()
504-
viewToSnapshot := map[*View]*snapshot{}
504+
viewToSnapshot := make(map[*View]source.Snapshot)
505505
for view, changed := range views {
506-
snapshot, release := view.invalidateContent(ctx, changed)
506+
snapshot, release := view.Invalidate(ctx, source.StateChange{Files: changed})
507507
releases = append(releases, release)
508508
viewToSnapshot[view] = snapshot
509509
}

gopls/internal/lsp/cache/snapshot.go

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import (
4040
"golang.org/x/tools/gopls/internal/lsp/source/xrefs"
4141
"golang.org/x/tools/gopls/internal/persistent"
4242
"golang.org/x/tools/gopls/internal/span"
43+
"golang.org/x/tools/gopls/internal/vulncheck"
44+
"golang.org/x/tools/internal/constraints"
4345
"golang.org/x/tools/internal/event"
4446
"golang.org/x/tools/internal/event/tag"
4547
"golang.org/x/tools/internal/gocommand"
@@ -177,6 +179,13 @@ type snapshot struct {
177179
// detect ignored files.
178180
ignoreFilterOnce sync.Once
179181
ignoreFilter *ignoreFilter
182+
183+
// moduleUpgrades tracks known upgrades for module paths in each modfile.
184+
// Each modfile has a map of module name to upgrade version.
185+
moduleUpgrades *persistent.Map[span.URI, map[string]string]
186+
187+
// vulns maps each go.mod file's URI to its known vulnerabilities.
188+
vulns *persistent.Map[span.URI, *vulncheck.Result]
180189
}
181190

182191
var globalSnapshotID uint64
@@ -251,6 +260,8 @@ func (s *snapshot) destroy(destroyedBy string) {
251260
s.modVulnHandles.Destroy()
252261
s.modWhyHandles.Destroy()
253262
s.unloadableFiles.Destroy()
263+
s.moduleUpgrades.Destroy()
264+
s.vulns.Destroy()
254265
}
255266

256267
func (s *snapshot) SequenceID() uint64 {
@@ -1814,7 +1825,8 @@ func inVendor(uri span.URI) bool {
18141825
return found && strings.Contains(after, "/")
18151826
}
18161827

1817-
func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) {
1828+
func (s *snapshot) clone(ctx, bgCtx context.Context, changed source.StateChange) (*snapshot, func()) {
1829+
changedFiles := changed.Files
18181830
ctx, done := event.Start(ctx, "cache.snapshot.clone")
18191831
defer done()
18201832

@@ -1834,20 +1846,22 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source
18341846
initializedErr: s.initializedErr,
18351847
packages: s.packages.Clone(),
18361848
activePackages: s.activePackages.Clone(),
1837-
files: s.files.Clone(changes),
1838-
symbolizeHandles: cloneWithout(s.symbolizeHandles, changes),
1849+
files: s.files.Clone(changedFiles),
1850+
symbolizeHandles: cloneWithout(s.symbolizeHandles, changedFiles),
18391851
workspacePackages: s.workspacePackages,
18401852
shouldLoad: s.shouldLoad.Clone(), // not cloneWithout: shouldLoad is cleared on loads
18411853
unloadableFiles: s.unloadableFiles.Clone(), // not cloneWithout: typing in a file doesn't necessarily make it loadable
1842-
parseModHandles: cloneWithout(s.parseModHandles, changes),
1843-
parseWorkHandles: cloneWithout(s.parseWorkHandles, changes),
1844-
modTidyHandles: cloneWithout(s.modTidyHandles, changes),
1845-
modWhyHandles: cloneWithout(s.modWhyHandles, changes),
1846-
modVulnHandles: cloneWithout(s.modVulnHandles, changes),
1854+
parseModHandles: cloneWithout(s.parseModHandles, changedFiles),
1855+
parseWorkHandles: cloneWithout(s.parseWorkHandles, changedFiles),
1856+
modTidyHandles: cloneWithout(s.modTidyHandles, changedFiles),
1857+
modWhyHandles: cloneWithout(s.modWhyHandles, changedFiles),
1858+
modVulnHandles: cloneWithout(s.modVulnHandles, changedFiles),
18471859
workspaceModFiles: s.workspaceModFiles,
18481860
workspaceModFilesErr: s.workspaceModFilesErr,
18491861
importGraph: s.importGraph,
18501862
pkgIndex: s.pkgIndex,
1863+
moduleUpgrades: cloneWith(s.moduleUpgrades, changed.ModuleUpgrades),
1864+
vulns: cloneWith(s.vulns, changed.Vulns),
18511865
}
18521866

18531867
// Create a lease on the new snapshot.
@@ -1864,7 +1878,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source
18641878
// vendor tree after 'go mod vendor' or 'rm -fr vendor/'.
18651879
//
18661880
// TODO(rfindley): revisit the location of this check.
1867-
for uri := range changes {
1881+
for uri := range changedFiles {
18681882
if inVendor(uri) && s.initializedErr != nil ||
18691883
strings.HasSuffix(string(uri), "/vendor/modules.txt") {
18701884
reinit = true
@@ -1877,7 +1891,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source
18771891
// where a file is added on disk; we don't want to read the newly added file
18781892
// into the old snapshot, as that will break our change detection below.
18791893
oldFiles := make(map[span.URI]source.FileHandle)
1880-
for uri := range changes {
1894+
for uri := range changedFiles {
18811895
if fh, ok := s.files.Get(uri); ok {
18821896
oldFiles[uri] = fh
18831897
}
@@ -1898,7 +1912,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source
18981912
}
18991913

19001914
if workURI, _ := s.view.GOWORK(); workURI != "" {
1901-
if newFH, ok := changes[workURI]; ok {
1915+
if newFH, ok := changedFiles[workURI]; ok {
19021916
result.workspaceModFiles, result.workspaceModFilesErr = computeWorkspaceModFiles(ctx, s.view.gomod, workURI, s.view.effectiveGO111MODULE(), result)
19031917
if changedOnDisk(oldFiles[workURI], newFH) {
19041918
reinit = true
@@ -1907,14 +1921,14 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source
19071921
}
19081922

19091923
// Reinitialize if any workspace mod file has changed on disk.
1910-
for uri, newFH := range changes {
1924+
for uri, newFH := range changedFiles {
19111925
if _, ok := result.workspaceModFiles[uri]; ok && changedOnDisk(oldFiles[uri], newFH) {
19121926
reinit = true
19131927
}
19141928
}
19151929

19161930
// Finally, process sumfile changes that may affect loading.
1917-
for uri, newFH := range changes {
1931+
for uri, newFH := range changedFiles {
19181932
if !changedOnDisk(oldFiles[uri], newFH) {
19191933
continue // like with go.mod files, we only reinit when things change on disk
19201934
}
@@ -1955,7 +1969,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source
19551969
anyFileOpenedOrClosed := false // opened files affect workspace packages
19561970
anyFileAdded := false // adding a file can resolve missing dependencies
19571971

1958-
for uri, newFH := range changes {
1972+
for uri, newFH := range changedFiles {
19591973
// The original FileHandle for this URI is cached on the snapshot.
19601974
oldFH, _ := oldFiles[uri] // may be nil
19611975
_, oldOpen := oldFH.(*Overlay)
@@ -2160,14 +2174,24 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source
21602174
return result, release
21612175
}
21622176

2163-
func cloneWithout[V any](m *persistent.Map[span.URI, V], changes map[span.URI]source.FileHandle) *persistent.Map[span.URI, V] {
2177+
// cloneWithout clones m then deletes from it the keys of changes.
2178+
func cloneWithout[K constraints.Ordered, V1, V2 any](m *persistent.Map[K, V1], changes map[K]V2) *persistent.Map[K, V1] {
21642179
m2 := m.Clone()
21652180
for k := range changes {
21662181
m2.Delete(k)
21672182
}
21682183
return m2
21692184
}
21702185

2186+
// cloneWith clones m then inserts the changes into it.
2187+
func cloneWith[K constraints.Ordered, V any](m *persistent.Map[K, V], changes map[K]V) *persistent.Map[K, V] {
2188+
m2 := m.Clone()
2189+
for k, v := range changes {
2190+
m2.Set(k, v, nil)
2191+
}
2192+
return m2
2193+
}
2194+
21712195
// deleteMostRelevantModFile deletes the mod file most likely to be the mod
21722196
// file for the changed URI, if it exists.
21732197
//

gopls/internal/lsp/cache/view.go

Lines changed: 26 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,6 @@ type View struct {
6565

6666
importsState *importsState
6767

68-
// moduleUpgrades tracks known upgrades for module paths in each modfile.
69-
// Each modfile has a map of module name to upgrade version.
70-
moduleUpgradesMu sync.Mutex
71-
moduleUpgrades map[span.URI]map[string]string
72-
73-
// vulns maps each go.mod file's URI to its known vulnerabilities.
74-
vulnsMu sync.Mutex
75-
vulns map[span.URI]*vulncheck.Result
76-
7768
// parseCache holds an LRU cache of recently parsed files.
7869
parseCache *parseCache
7970

@@ -864,14 +855,13 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadEr
864855
return loadErr
865856
}
866857

867-
// invalidateContent invalidates the content of a Go file,
868-
// including any position and type information that depends on it.
858+
// Invalidate processes the provided state change, invalidating any derived
859+
// results that depend on the changed state.
869860
//
870-
// invalidateContent returns a non-nil snapshot for the new content, along with
871-
// a callback which the caller must invoke to release that snapshot.
872-
//
873-
// newOptions may be nil, in which case options remain unchanged.
874-
func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) {
861+
// The resulting snapshot is non-nil, representing the outcome of the state
862+
// change. The second result is a function that must be called to release the
863+
// snapshot when the snapshot is no longer needed.
864+
func (v *View) Invalidate(ctx context.Context, changed source.StateChange) (source.Snapshot, func()) {
875865
// Detach the context so that content invalidation cannot be canceled.
876866
ctx = xcontext.Detach(ctx)
877867

@@ -893,7 +883,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]sourc
893883
prevSnapshot.AwaitInitialized(ctx)
894884

895885
// Save one lease of the cloned snapshot in the view.
896-
v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes)
886+
v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changed)
897887

898888
prevReleaseSnapshot()
899889
v.destroy(prevSnapshot, "View.invalidateContent")
@@ -1037,75 +1027,44 @@ func (v *View) IsGoPrivatePath(target string) bool {
10371027
return globsMatchPath(v.goprivate, target)
10381028
}
10391029

1040-
func (v *View) ModuleUpgrades(modfile span.URI) map[string]string {
1041-
v.moduleUpgradesMu.Lock()
1042-
defer v.moduleUpgradesMu.Unlock()
1043-
1030+
func (s *snapshot) ModuleUpgrades(modfile span.URI) map[string]string {
1031+
s.mu.Lock()
1032+
defer s.mu.Unlock()
10441033
upgrades := map[string]string{}
1045-
for mod, ver := range v.moduleUpgrades[modfile] {
1034+
orig, _ := s.moduleUpgrades.Get(modfile)
1035+
for mod, ver := range orig {
10461036
upgrades[mod] = ver
10471037
}
10481038
return upgrades
10491039
}
10501040

1051-
func (v *View) RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) {
1052-
// Return early if there are no upgrades.
1053-
if len(upgrades) == 0 {
1054-
return
1055-
}
1056-
1057-
v.moduleUpgradesMu.Lock()
1058-
defer v.moduleUpgradesMu.Unlock()
1059-
1060-
m := v.moduleUpgrades[modfile]
1061-
if m == nil {
1062-
m = make(map[string]string)
1063-
v.moduleUpgrades[modfile] = m
1064-
}
1065-
for mod, ver := range upgrades {
1066-
m[mod] = ver
1067-
}
1068-
}
1069-
1070-
func (v *View) ClearModuleUpgrades(modfile span.URI) {
1071-
v.moduleUpgradesMu.Lock()
1072-
defer v.moduleUpgradesMu.Unlock()
1073-
1074-
delete(v.moduleUpgrades, modfile)
1075-
}
1076-
1077-
const maxGovulncheckResultAge = 1 * time.Hour // Invalidate results older than this limit.
1078-
var timeNow = time.Now // for testing
1041+
// MaxGovulncheckResultsAge defines the maximum vulnerability age considered
1042+
// valid by gopls.
1043+
//
1044+
// Mutable for testing.
1045+
var MaxGovulncheckResultAge = 1 * time.Hour
10791046

1080-
func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*vulncheck.Result {
1047+
// TODO(rfindley): move to snapshot.go
1048+
func (s *snapshot) Vulnerabilities(modfiles ...span.URI) map[span.URI]*vulncheck.Result {
10811049
m := make(map[span.URI]*vulncheck.Result)
1082-
now := timeNow()
1083-
v.vulnsMu.Lock()
1084-
defer v.vulnsMu.Unlock()
1050+
now := time.Now()
1051+
1052+
s.mu.Lock()
1053+
defer s.mu.Unlock()
10851054

10861055
if len(modfiles) == 0 { // empty means all modfiles
1087-
for modfile := range v.vulns {
1088-
modfiles = append(modfiles, modfile)
1089-
}
1056+
modfiles = s.vulns.Keys()
10901057
}
10911058
for _, modfile := range modfiles {
1092-
vuln := v.vulns[modfile]
1093-
if vuln != nil && now.Sub(vuln.AsOf) > maxGovulncheckResultAge {
1094-
v.vulns[modfile] = nil // same as SetVulnerabilities(modfile, nil)
1059+
vuln, _ := s.vulns.Get(modfile)
1060+
if vuln != nil && now.Sub(vuln.AsOf) > MaxGovulncheckResultAge {
10951061
vuln = nil
10961062
}
10971063
m[modfile] = vuln
10981064
}
10991065
return m
11001066
}
11011067

1102-
func (v *View) SetVulnerabilities(modfile span.URI, vulns *vulncheck.Result) {
1103-
v.vulnsMu.Lock()
1104-
defer v.vulnsMu.Unlock()
1105-
1106-
v.vulns[modfile] = vulns
1107-
}
1108-
11091068
func (v *View) GoVersion() int {
11101069
return v.workspaceInformation.goversion
11111070
}

0 commit comments

Comments
 (0)