Skip to content

Commit b0dc9b1

Browse files
committed
gopls/internal/filecache: Get: mitigate failure due to ENOSPC
filecache.Get operations sometimes fail. This CL enumerates a number of causes, and mitigates the likely most common one--failure to create the cache due to ENOSPC--by forcing cache creation during early startup. This also minimizes the time window during which deletion of the gopls executable is a possible cause. If we continue to observe failures, the mostly likely remaining cause is deletion of the cache while gopls is running. This CL details a possible mitigation. Updates golang/go#67433 Change-Id: I3545e56f7af308afba3527a418757b3cf4573569 Reviewed-on: https://go-review.googlesource.com/c/tools/+/639395 Reviewed-by: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 71b5dfe commit b0dc9b1

File tree

2 files changed

+39
-4
lines changed

2 files changed

+39
-4
lines changed

gopls/internal/filecache/filecache.go

+18-4
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ type memKey struct {
6262

6363
// Get retrieves from the cache and returns the value most recently
6464
// supplied to Set(kind, key), possibly by another process.
65-
// Get returns ErrNotFound if the value was not found.
65+
//
66+
// Get returns ErrNotFound if the value was not found. The first call
67+
// to Get may fail due to ENOSPC or deletion of the process's
68+
// executable. Other causes of failure include deletion or corruption
69+
// of the cache (by external meddling) while gopls is running, or
70+
// faulty hardware; see issue #67433.
6671
//
6772
// Callers should not modify the returned array.
6873
func Get(kind string, key [32]byte) ([]byte, error) {
@@ -79,6 +84,8 @@ func Get(kind string, key [32]byte) ([]byte, error) {
7984
// Read the index file, which provides the name of the CAS file.
8085
indexName, err := filename(kind, key)
8186
if err != nil {
87+
// e.g. ENOSPC, deletion of executable (first time only);
88+
// deletion of cache (at any time).
8289
return nil, err
8390
}
8491
indexData, err := os.ReadFile(indexName)
@@ -100,7 +107,7 @@ func Get(kind string, key [32]byte) ([]byte, error) {
100107
// engineered hash collision, which is infeasible.
101108
casName, err := filename(casKind, valueHash)
102109
if err != nil {
103-
return nil, err
110+
return nil, err // see above for possible causes
104111
}
105112
value, _ := os.ReadFile(casName) // ignore error
106113
if sha256.Sum256(value) != valueHash {
@@ -138,6 +145,13 @@ func Get(kind string, key [32]byte) ([]byte, error) {
138145
var ErrNotFound = fmt.Errorf("not found")
139146

140147
// Set updates the value in the cache.
148+
//
149+
// Set may fail due to:
150+
// - failure to access/create the cache (first call only);
151+
// - out of space (ENOSPC);
152+
// - deletion of the cache concurrent with a call to Set;
153+
// - faulty hardware.
154+
// See issue #67433.
141155
func Set(kind string, key [32]byte, value []byte) error {
142156
memCache.Set(memKey{kind, key}, value, len(value))
143157

@@ -353,13 +367,13 @@ func getCacheDir() (string, error) {
353367
// Compute the hash of this executable (~20ms) and create a subdirectory.
354368
hash, err := hashExecutable()
355369
if err != nil {
356-
cacheDirErr = fmt.Errorf("can't hash gopls executable: %v", err)
370+
cacheDirErr = fmt.Errorf("can't hash gopls executable: %w", err)
357371
}
358372
// Use only 32 bits of the digest to avoid unwieldy filenames.
359373
// It's not an adversarial situation.
360374
cacheDir = filepath.Join(goplsDir, fmt.Sprintf("%x", hash[:4]))
361375
if err := os.MkdirAll(cacheDir, 0700); err != nil {
362-
cacheDirErr = fmt.Errorf("can't create cache: %v", err)
376+
cacheDirErr = fmt.Errorf("can't create cache: %w", err)
363377
}
364378
})
365379
return cacheDir, cacheDirErr

gopls/main.go

+21
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ package main // import "golang.org/x/tools/gopls"
1313

1414
import (
1515
"context"
16+
"log"
1617
"os"
1718

1819
"golang.org/x/telemetry"
20+
"golang.org/x/telemetry/counter"
1921
"golang.org/x/tools/gopls/internal/cmd"
22+
"golang.org/x/tools/gopls/internal/filecache"
2023
versionpkg "golang.org/x/tools/gopls/internal/version"
2124
"golang.org/x/tools/internal/tool"
2225
)
@@ -31,6 +34,24 @@ func main() {
3134
Upload: true,
3235
})
3336

37+
// Force early creation of the filecache and refuse to start
38+
// if there were unexpected errors such as ENOSPC. This
39+
// minimizes the window of exposure to deletion of the
40+
// executable, and ensures that all subsequent calls to
41+
// filecache.Get cannot fail for these two reasons;
42+
// see issue #67433.
43+
//
44+
// This leaves only one likely cause for later failures:
45+
// deletion of the cache while gopls is running. If the
46+
// problem continues, we could periodically stat the cache
47+
// directory (for example at the start of every RPC) and
48+
// either re-create it or just fail the RPC with an
49+
// informative error and terminate the process.
50+
if _, err := filecache.Get("nonesuch", [32]byte{}); err != nil && err != filecache.ErrNotFound {
51+
counter.Inc("gopls/nocache")
52+
log.Fatalf("gopls cannot access its persistent index (disk full?): %v", err)
53+
}
54+
3455
ctx := context.Background()
3556
tool.Main(ctx, cmd.New(), os.Args[1:])
3657
}

0 commit comments

Comments
 (0)