gopls/internal/cache: don't mark initialized after cancellation
When a snapshot reinitialization is canceled, we should not mark it as
complete. Initialization is special, in that it does not set
GOPROXY=off. Canceling it can result in subsequent load failures.
Fixes golang/go#57626
Change-Id: I9bc2c9226539fa8d240ecf2be62cc8d9915e5642
Reviewed-on: https://go-review.googlesource.com/c/tools/+/460795
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go
index a873b50..c2ad0d4 100644
--- a/gopls/internal/lsp/cache/view.go
+++ b/gopls/internal/lsp/cache/view.go
@@ -657,11 +657,22 @@
s.collectAllKnownSubdirs(ctx)
}
-func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) {
+func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadErr error) {
+ // A failure is retryable if it may have been due to context cancellation,
+ // and this is not the initial workspace load (firstAttempt==true).
+ //
+ // The IWL runs on a detached context with a long (~10m) timeout, so
+ // if the context was canceled we consider loading to have failed
+ // permanently.
+ retryableFailure := func() bool {
+ return loadErr != nil && ctx.Err() != nil && !firstAttempt
+ }
defer func() {
- s.mu.Lock()
- s.initialized = true
- s.mu.Unlock()
+ if !retryableFailure() {
+ s.mu.Lock()
+ s.initialized = true
+ s.mu.Unlock()
+ }
if firstAttempt {
close(s.view.initialWorkspaceLoad)
}
@@ -719,26 +730,24 @@
if len(scopes) > 0 {
scopes = append(scopes, packageLoadScope("builtin"))
}
- err := s.load(ctx, true, scopes...)
+ loadErr = s.load(ctx, true, scopes...)
- // If the context is canceled on the first attempt, loading has failed
- // because the go command has timed out--that should be a critical error.
- if err != nil && !firstAttempt && ctx.Err() != nil {
- return
+ if retryableFailure() {
+ return loadErr
}
var criticalErr *source.CriticalError
switch {
- case err != nil && ctx.Err() != nil:
- event.Error(ctx, fmt.Sprintf("initial workspace load: %v", err), err)
+ case loadErr != nil && ctx.Err() != nil:
+ event.Error(ctx, fmt.Sprintf("initial workspace load: %v", loadErr), loadErr)
criticalErr = &source.CriticalError{
- MainError: err,
+ MainError: loadErr,
}
- case err != nil:
- event.Error(ctx, "initial workspace load failed", err)
- extractedDiags := s.extractGoCommandErrors(ctx, err)
+ case loadErr != nil:
+ event.Error(ctx, "initial workspace load failed", loadErr)
+ extractedDiags := s.extractGoCommandErrors(ctx, loadErr)
criticalErr = &source.CriticalError{
- MainError: err,
+ MainError: loadErr,
Diagnostics: append(modDiagnostics, extractedDiags...),
}
case len(modDiagnostics) == 1:
@@ -757,6 +766,7 @@
s.mu.Lock()
defer s.mu.Unlock()
s.initializedErr = criticalErr
+ return loadErr
}
// invalidateContent invalidates the content of a Go file,
diff --git a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go
index c30a075..3b623d0 100644
--- a/gopls/internal/lsp/regtest/expectation.go
+++ b/gopls/internal/lsp/regtest/expectation.go
@@ -328,6 +328,13 @@
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves, true)
}
+// StartedChangeWatchedFiles expects that the server has at least started
+// processing all didChangeWatchedFiles notifications sent from the client.
+func (e *Env) StartedChangeWatchedFiles() Expectation {
+ changes := e.Editor.Stats().DidChangeWatchedFiles
+ return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes)
+}
+
// DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications
// currently sent by the editor to be completely processed.
func (e *Env) DoneWithChangeWatchedFiles() Expectation {
diff --git a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go
index 55bc01e..4620b93 100644
--- a/gopls/internal/regtest/workspace/metadata_test.go
+++ b/gopls/internal/regtest/workspace/metadata_test.go
@@ -5,6 +5,7 @@
package workspace
import (
+ "strings"
"testing"
. "golang.org/x/tools/gopls/internal/lsp/regtest"
@@ -109,3 +110,80 @@
)
})
}
+
+func TestReinitializeRepeatedly(t *testing.T) {
+ testenv.NeedsGo1Point(t, 18) // uses go.work
+
+ const multiModule = `
+-- go.work --
+go 1.18
+
+use (
+ moda/a
+ modb
+)
+-- moda/a/go.mod --
+module a.com
+
+require b.com v1.2.3
+-- moda/a/go.sum --
+b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI=
+b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8=
+-- moda/a/a.go --
+package a
+
+import (
+ "b.com/b"
+)
+
+func main() {
+ var x int
+ _ = b.Hello()
+ // AAA
+}
+-- modb/go.mod --
+module b.com
+
+-- modb/b/b.go --
+package b
+
+func Hello() int {
+ var x int
+}
+`
+ WithOptions(
+ ProxyFiles(workspaceModuleProxy),
+ Settings{
+ // For this test, we want workspace diagnostics to start immediately
+ // during change processing.
+ "diagnosticsDelay": "0",
+ },
+ ).Run(t, multiModule, func(t *testing.T, env *Env) {
+ env.OpenFile("moda/a/a.go")
+ env.AfterChange()
+
+ // This test verifies that we fully process workspace reinitialization
+ // (which allows GOPROXY), even when the reinitialized snapshot is
+ // invalidated by subsequent changes.
+ //
+ // First, update go.work to remove modb. This will cause reinitialization
+ // to fetch b.com from the proxy.
+ env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a")
+ // Next, wait for gopls to start processing the change. Because we've set
+ // diagnosticsDelay to zero, this will start diagnosing the workspace (and
+ // try to reinitialize on the snapshot context).
+ env.Await(env.StartedChangeWatchedFiles())
+ // Finally, immediately make a file change to cancel the previous
+ // operation. This is racy, but will usually cause initialization to be
+ // canceled.
+ env.RegexpReplace("moda/a/a.go", "AAA", "BBB")
+ env.AfterChange()
+ // Now, to satisfy a definition request, gopls will try to reload moda. But
+ // without access to the proxy (because this is no longer a
+ // reinitialization), this loading will fail.
+ got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
+ if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) {
+ t.Errorf("expected %s, got %v", want, got)
+ }
+ })
+}