gopls/internal/cache: stop module cache refresh on view shutdown

As described in golang/go#67865, CL 590377 exacerbated a problem that
module cache refreshes may outlive the lifecycle of the view.

Fixes golang/go#67865

Change-Id: Ieafdf6601fee00b6e8ce705502a80224da071578
Reviewed-on: https://go-review.googlesource.com/c/tools/+/591315
Auto-Submit: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/cache/imports.go b/gopls/internal/cache/imports.go
index 76a5855..c467a85 100644
--- a/gopls/internal/cache/imports.go
+++ b/gopls/internal/cache/imports.go
@@ -35,6 +35,18 @@
 	}
 }
 
+// stop stops any future scheduled refresh.
+func (t *refreshTimer) stop() {
+	t.mu.Lock()
+	defer t.mu.Unlock()
+
+	if t.timer != nil {
+		t.timer.Stop()
+		t.timer = nil
+		t.refreshFn = nil // release resources
+	}
+}
+
 // schedule schedules the refresh function to run at some point in the future,
 // if no existing refresh is already scheduled.
 //
@@ -54,11 +66,16 @@
 		}
 		t.timer = time.AfterFunc(delay, func() {
 			start := time.Now()
-			t.refreshFn()
 			t.mu.Lock()
-			t.duration = time.Since(start)
-			t.timer = nil
+			refreshFn := t.refreshFn
 			t.mu.Unlock()
+			if refreshFn != nil { // timer may be stopped.
+				refreshFn()
+				t.mu.Lock()
+				t.duration = time.Since(start)
+				t.timer = nil
+				t.mu.Unlock()
+			}
 		})
 	}
 }
@@ -70,7 +87,8 @@
 type sharedModCache struct {
 	mu     sync.Mutex
 	caches map[string]*imports.DirInfoCache // GOMODCACHE -> cache content; never invalidated
-	timers map[string]*refreshTimer         // GOMODCACHE -> timer
+	// TODO(rfindley): consider stopping these timers when the session shuts down.
+	timers map[string]*refreshTimer // GOMODCACHE -> timer
 }
 
 func (c *sharedModCache) dirCache(dir string) *imports.DirInfoCache {
@@ -131,6 +149,11 @@
 	return s
 }
 
+// stopTimer stops scheduled refreshes of this imports state.
+func (s *importsState) stopTimer() {
+	s.refreshTimer.stop()
+}
+
 // runProcessEnvFunc runs goimports.
 //
 // Any call to runProcessEnvFunc will schedule a refresh of the imports state
diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go
index 6f76c55..ab77e63 100644
--- a/gopls/internal/cache/view.go
+++ b/gopls/internal/cache/view.go
@@ -470,6 +470,7 @@
 func (v *View) shutdown() {
 	// Cancel the initial workspace load if it is still running.
 	v.cancelInitialWorkspaceLoad()
+	v.importsState.stopTimer()
 
 	v.snapshotMu.Lock()
 	if v.snapshot != nil {