1
0
mirror of https://github.com/golang/go synced 2024-10-01 11:28:34 -06:00
go/internal/lsp/cache/load.go
Rebecca Stambler 4f238b926e internal/lsp: fix deadlock between f.mu and f.handleMu
This change propagates the file handle through the type-checking
process, ensuring that the same handle is used throughout. It also
removes the ordering constraint that f.mu needs to be acquired before
f.handleMu. To make this more correct, we should associate a cached
package only with a FileHandle, but this relies on correct cache
invalidation, so that will be addressed in future changes.

Updates golang/go#34052

Change-Id: I6645046bfd15c882619a7f01f9b48c838de42a30
Reviewed-on: https://go-review.googlesource.com/c/tools/+/193718
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2019-09-05 22:17:16 +00:00

343 lines
9.6 KiB
Go

// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cache
import (
"context"
"fmt"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/tag"
"golang.org/x/tools/internal/telemetry/trace"
errors "golang.org/x/xerrors"
)
func (view *view) loadParseTypecheck(ctx context.Context, f *goFile, fh source.FileHandle) error {
ctx, done := trace.StartSpan(ctx, "cache.view.loadParseTypeCheck", telemetry.URI.Of(f.URI()))
defer done()
meta, err := view.load(ctx, f, fh)
if err != nil {
return err
}
for _, m := range meta {
imp := &importer{
view: view,
config: view.Config(ctx),
seen: make(map[packageID]struct{}),
topLevelPackageID: m.id,
}
cph, err := imp.checkPackageHandle(ctx, m)
if err != nil {
log.Error(ctx, "loadParseTypeCheck: failed to get CheckPackageHandle", err, telemetry.Package.Of(m.id))
continue
}
if _, err := cph.check(ctx); err != nil {
log.Error(ctx, "loadParseTypeCheck: failed to check package", err, telemetry.Package.Of(m.id))
continue
}
// Cache this package on the file object, since all dependencies are cached in the Import function.
if err := imp.cachePackage(ctx, cph); err != nil {
log.Error(ctx, "loadParseTypeCheck: failed to cache package", err, telemetry.Package.Of(m.id))
continue
}
}
return nil
}
func (view *view) load(ctx context.Context, f *goFile, fh source.FileHandle) ([]*metadata, error) {
ctx, done := trace.StartSpan(ctx, "cache.view.load", telemetry.URI.Of(f.URI()))
defer done()
view.mu.Lock()
defer view.mu.Unlock()
view.mcache.mu.Lock()
defer view.mcache.mu.Unlock()
var toDelete []packageID
f.mu.Lock()
for id, cph := range f.pkgs {
if cph != nil {
toDelete = append(toDelete, id)
}
}
f.mu.Unlock()
// If the AST for this file is trimmed, and we are explicitly type-checking it,
// don't ignore function bodies.
if f.wrongParseMode(ctx, fh, source.ParseFull) {
// Remove the package and all of its reverse dependencies from the cache.
for _, id := range toDelete {
f.view.remove(ctx, id, map[packageID]struct{}{})
}
}
// Get the metadata for the file.
meta, err := view.checkMetadata(ctx, f, fh)
if err != nil {
return nil, err
}
if len(meta) == 0 {
return nil, fmt.Errorf("no package metadata found for %s", f.URI())
}
return meta, nil
}
// checkMetadata determines if we should run go/packages.Load for this file.
// If yes, update the metadata for the file and its package.
func (v *view) checkMetadata(ctx context.Context, f *goFile, fh source.FileHandle) ([]*metadata, error) {
// Check if we need to re-run go/packages before loading the package.
f.mu.Lock()
runGopackages := v.shouldRunGopackages(ctx, f, fh)
metadata := f.metadata()
f.mu.Unlock()
// The package metadata is correct as-is, so just return it.
if !runGopackages {
return metadata, nil
}
// Check if the context has been canceled before calling packages.Load.
if ctx.Err() != nil {
return nil, ctx.Err()
}
ctx, done := trace.StartSpan(ctx, "packages.Load", telemetry.File.Of(f.filename()))
defer done()
pkgs, err := packages.Load(v.Config(ctx), fmt.Sprintf("file=%s", f.filename()))
if len(pkgs) == 0 {
if err == nil {
err = errors.Errorf("go/packages.Load: no packages found for %s", f.filename())
}
// Return this error as a diagnostic to the user.
return nil, err
}
// Track missing imports as we look at the package's errors.
missingImports := make(map[packagePath]struct{})
log.Print(ctx, "go/packages.Load", tag.Of("packages", len(pkgs)))
for _, pkg := range pkgs {
log.Print(ctx, "go/packages.Load", tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles))
// Build the import graph for this package.
if err := v.link(ctx, &importGraph{
pkgPath: packagePath(pkg.PkgPath),
pkg: pkg,
parent: nil,
missingImports: make(map[packagePath]struct{}),
}); err != nil {
return nil, err
}
}
m, err := validateMetadata(ctx, missingImports, f)
if err != nil {
return nil, err
}
return m, nil
}
func validateMetadata(ctx context.Context, missingImports map[packagePath]struct{}, f *goFile) ([]*metadata, error) {
f.mu.Lock()
defer f.mu.Unlock()
// If `go list` failed to get data for the file in question (this should never happen).
if len(f.meta) == 0 {
return nil, errors.Errorf("loadParseTypecheck: no metadata found for %v", f.filename())
}
// If we have already seen these missing imports before, and we have type information,
// there is no need to continue.
if sameSet(missingImports, f.missingImports) && len(f.pkgs) != 0 {
return nil, nil
}
// Otherwise, update the missing imports map.
f.missingImports = missingImports
return f.metadata(), nil
}
func sameSet(x, y map[packagePath]struct{}) bool {
if len(x) != len(y) {
return false
}
for k := range x {
if _, ok := y[k]; !ok {
return false
}
}
return true
}
// shouldRunGopackages reparses a file's package and import declarations to
// determine if they have changed.
// It assumes that the caller holds the lock on the f.mu lock.
func (v *view) shouldRunGopackages(ctx context.Context, f *goFile, fh source.FileHandle) (result bool) {
defer func() {
// Clear metadata if we are intending to re-run go/packages.
if result {
// Reset the file's metadata and type information if we are re-running `go list`.
for k := range f.meta {
delete(f.meta, k)
}
for k := range f.pkgs {
delete(f.pkgs, k)
}
}
}()
if len(f.meta) == 0 || len(f.missingImports) > 0 {
return true
}
// Get file content in case we don't already have it.
parsed, err := v.session.cache.ParseGoHandle(fh, source.ParseHeader).Parse(ctx)
if err == context.Canceled {
log.Error(ctx, "parsing file header", err, tag.Of("file", f.URI()))
return false
}
if parsed == nil {
return true
}
// Check if the package's name has changed, by checking if this is a filename
// we already know about, and if so, check if its package name has changed.
for _, m := range f.meta {
for _, uri := range m.files {
if span.CompareURI(uri, f.URI()) == 0 {
if m.name != parsed.Name.Name {
return true
}
}
}
}
// If the package's imports have changed, re-run `go list`.
if len(f.imports) != len(parsed.Imports) {
return true
}
for i, importSpec := range f.imports {
if importSpec.Path.Value != parsed.Imports[i].Path.Value {
return true
}
}
return false
}
type importGraph struct {
pkgPath packagePath
pkg *packages.Package
parent *metadata
missingImports map[packagePath]struct{}
}
func (v *view) link(ctx context.Context, g *importGraph) error {
// Recreate the metadata rather than reusing it to avoid locking.
m := &metadata{
id: packageID(g.pkg.ID),
pkgPath: g.pkgPath,
name: g.pkg.Name,
typesSizes: g.pkg.TypesSizes,
errors: g.pkg.Errors,
}
for _, filename := range g.pkg.CompiledGoFiles {
m.files = append(m.files, span.FileURI(filename))
// Call the unlocked version of getFile since we are holding the view's mutex.
f, err := v.getFile(ctx, span.FileURI(filename))
if err != nil {
log.Error(ctx, "no file", err, telemetry.File.Of(filename))
continue
}
gof, ok := f.(*goFile)
if !ok {
log.Error(ctx, "not a Go file", nil, telemetry.File.Of(filename))
continue
}
// Cache the metadata for this file.
gof.mu.Lock()
if gof.meta == nil {
gof.meta = make(map[packageID]*metadata)
}
gof.meta[m.id] = m
gof.mu.Unlock()
}
// Preserve the import graph.
if original, ok := v.mcache.packages[m.id]; ok {
m.children = original.children
m.parents = original.parents
}
if m.children == nil {
m.children = make(map[packageID]*metadata)
}
if m.parents == nil {
m.parents = make(map[packageID]bool)
}
// Add the metadata to the cache.
v.mcache.packages[m.id] = m
v.mcache.ids[g.pkgPath] = m.id
// Connect the import graph.
if g.parent != nil {
m.parents[g.parent.id] = true
g.parent.children[m.id] = m
}
for importPath, importPkg := range g.pkg.Imports {
importPkgPath := packagePath(importPath)
if importPkgPath == g.pkgPath {
return fmt.Errorf("cycle detected in %s", importPath)
}
// Don't remember any imports with significant errors.
if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
g.missingImports[importPkgPath] = struct{}{}
continue
}
if _, ok := m.children[packageID(importPkg.ID)]; !ok {
if err := v.link(ctx, &importGraph{
pkgPath: importPkgPath,
pkg: importPkg,
parent: m,
missingImports: g.missingImports,
}); err != nil {
log.Error(ctx, "error in dependecny", err)
}
}
}
// Clear out any imports that have been removed since the package was last loaded.
for importID := range m.children {
child, ok := v.mcache.packages[importID]
if !ok {
continue
}
importPath := string(child.pkgPath)
if _, ok := g.pkg.Imports[importPath]; ok {
continue
}
delete(m.children, importID)
delete(child.parents, m.id)
}
return nil
}
func identicalFileHandles(old, new []source.ParseGoHandle) bool {
if len(old) != len(new) {
return false
}
oldByIdentity := make(map[string]struct{}, len(old))
for _, ph := range old {
oldByIdentity[hashParseKey(ph)] = struct{}{}
}
for _, ph := range new {
if _, found := oldByIdentity[hashParseKey(ph)]; !found {
return false
}
delete(oldByIdentity, hashParseKey(ph))
}
return len(oldByIdentity) == 0
}