gopls: run filecache validation asynchronously in the background Move the filecache validation check (which hashes the executable and verifies cache access) from the synchronous startup path in main.go into the background goroutine started by filecache.Start. Before this CL, this check was run synchronously in main.go, blocking the startup of all gopls commands (including quick commands like help and version) for ~50ms while the executable was hashed. By running the check asynchronously, short-lived commands can exit quickly before the check completes. Long-running commands (like serve) will still be terminated via log.Fatalf if the validation fails (e.g., due to disk full / ENOSPC) once the background check finishes. Concurrent gopls processes (e.g., CLI and daemon) won't conflict because GC is delayed and concurrent writes to the shared cache are handled safely. Updates golang/go#67433 Updates golang/go#79906 Change-Id: I63869dee81fa8ae176e762a291c765d148f67044 Reviewed-on: https://go-review.googlesource.com/c/tools/+/797082 Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/gopls/internal/cmd/harness.go b/gopls/internal/cmd/harness.go index 66d7241..134814c 100644 --- a/gopls/internal/cmd/harness.go +++ b/gopls/internal/cmd/harness.go
@@ -102,10 +102,12 @@ // In the category of "things we can do while waiting for the // Go command": - // TODO(hyangah): check if it's desirable to run filecache.Start unconditionally - // on every gopls subcommand, including CLI running with -remote or -help message. - // Pre-initialize the filecache, which takes ~50ms to hash the gopls - // executable, and immediately runs a gc. + // Pre-initialize the filecache as early as possible. + // The work (hashing the executable, starting GC, and checking cache access) + // is done in a separate goroutine, so it doesn't block startup of short-lived + // commands (like help/version) which will exit before the goroutine finishes. + // Concurrent gopls processes (e.g. CLI and daemon) won't conflict because + // GC is delayed and concurrent writes to the shared cache are handled safely. filecache.Start() ctx = debug.WithInstance(ctx, app.OTel)
diff --git a/gopls/internal/filecache/filecache.go b/gopls/internal/filecache/filecache.go index 184e2fb..44db48e 100644 --- a/gopls/internal/filecache/filecache.go +++ b/gopls/internal/filecache/filecache.go
@@ -38,16 +38,35 @@ "sync/atomic" "time" + "golang.org/x/telemetry/counter" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/lru" ) -// Start causes the filecache to initialize and start garbage gollection. +// Start causes the filecache to initialize and start garbage collection. // // Start is automatically called by the first call to Get, but may be called // explicitly to pre-initialize the cache. func Start() { - go getCacheDir() // ignore error + go func() { + // Force early creation of the filecache and refuse to start + // if there were unexpected errors such as ENOSPC. This + // minimizes the window of exposure to deletion of the + // executable, and ensures that all subsequent calls to + // filecache.Get cannot fail for these two reasons; + // see issue #67433. + // + // This leaves only one likely cause for later failures: + // deletion of the cache while gopls is running. If the + // problem continues, we could periodically stat the cache + // directory (for example at the start of every RPC) and + // either re-create it or just fail the RPC with an + // informative error and terminate the process. + if _, err := Get("nonesuch", [32]byte{}, Bytes); err != nil && err != ErrNotFound { + counter.Inc("gopls/nocache") + log.Fatalf("gopls cannot access its persistent index (disk full?): %v", err) + } + }() } // memCache is a 100MB in-memory LRU cache in front of filecache
diff --git a/gopls/main.go b/gopls/main.go index 75e4ca2..4343652 100644 --- a/gopls/main.go +++ b/gopls/main.go
@@ -11,12 +11,8 @@ package main import ( - "log" - "golang.org/x/telemetry" - "golang.org/x/telemetry/counter" "golang.org/x/tools/gopls/internal/cmd" - "golang.org/x/tools/gopls/internal/filecache" versionpkg "golang.org/x/tools/gopls/internal/version" ) @@ -30,23 +26,5 @@ Upload: true, }) - // Force early creation of the filecache and refuse to start - // if there were unexpected errors such as ENOSPC. This - // minimizes the window of exposure to deletion of the - // executable, and ensures that all subsequent calls to - // filecache.Get cannot fail for these two reasons; - // see issue #67433. - // - // This leaves only one likely cause for later failures: - // deletion of the cache while gopls is running. If the - // problem continues, we could periodically stat the cache - // directory (for example at the start of every RPC) and - // either re-create it or just fail the RPC with an - // informative error and terminate the process. - if _, err := filecache.Get("nonesuch", [32]byte{}, filecache.Bytes); err != nil && err != filecache.ErrNotFound { - counter.Inc("gopls/nocache") - log.Fatalf("gopls cannot access its persistent index (disk full?): %v", err) - } - cmd.Main() }