Skip to content

Commit f618651

Browse files
committed
internal/lsp/cache: compare file size when invalidating file cache
Our filesystem caching layer uses file modification time to invalidate file contents. This is an imperfect heuristic, and on certain operating systems with low resolution filesystem clocks (such as WSL), this can be broken in practice. A proper fix would be to just read the file contents directly and rely on the snapshot to optimize file access, but we don't know that this is a safe change. Instead, try to reduce the likelihood of false cache hits by also checking the file size reported by Stat. For golang/go#43554 Change-Id: I1af384db532725e84fa6f3a2e5469d10b43fee92 Reviewed-on: https://go-review.googlesource.com/c/tools/+/283053 Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]> Trust: Robert Findley <[email protected]>
1 parent 7646fae commit f618651

File tree

2 files changed

+26
-6
lines changed

2 files changed

+26
-6
lines changed

internal/lsp/cache/cache.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ type fileHandle struct {
5757
bytes []byte
5858
hash string
5959
err error
60+
61+
// size is the file length as reported by Stat, for the purpose of
62+
// invalidation. Probably we could just use len(bytes), but this is done
63+
// defensively in case the definition of file size in the file system
64+
// differs.
65+
size int64
6066
}
6167

6268
func (h *fileHandle) Saved() bool {
@@ -79,11 +85,23 @@ func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error)
7985
c.fileMu.Lock()
8086
fh, ok := c.fileContent[uri]
8187
c.fileMu.Unlock()
82-
if ok && fh.modTime.Equal(fi.ModTime()) {
88+
89+
// Check mtime and file size to infer whether the file has changed. This is
90+
// an imperfect heuristic. Notably on some real systems (such as WSL) the
91+
// filesystem clock resolution can be large -- 1/64s was observed. Therefore
92+
// it's quite possible for multiple file modifications to occur within a
93+
// single logical 'tick'. This can leave the cache in an incorrect state, but
94+
// unfortunately we can't afford to pay the price of reading the actual file
95+
// content here. Or to be more precise, reading would be a risky change and
96+
// we don't know if we can afford it.
97+
//
98+
// We check file size in an attempt to reduce the probability of false cache
99+
// hits.
100+
if ok && fh.modTime.Equal(fi.ModTime()) && fh.size == fi.Size() {
83101
return fh, nil
84102
}
85103

86-
fh, err := readFile(ctx, uri, fi.ModTime())
104+
fh, err := readFile(ctx, uri, fi)
87105
if err != nil {
88106
return nil, err
89107
}
@@ -96,7 +114,7 @@ func (c *Cache) getFile(ctx context.Context, uri span.URI) (*fileHandle, error)
96114
// ioLimit limits the number of parallel file reads per process.
97115
var ioLimit = make(chan struct{}, 128)
98116

99-
func readFile(ctx context.Context, uri span.URI, modTime time.Time) (*fileHandle, error) {
117+
func readFile(ctx context.Context, uri span.URI, fi os.FileInfo) (*fileHandle, error) {
100118
select {
101119
case ioLimit <- struct{}{}:
102120
case <-ctx.Done():
@@ -111,12 +129,14 @@ func readFile(ctx context.Context, uri span.URI, modTime time.Time) (*fileHandle
111129
data, err := ioutil.ReadFile(uri.Filename())
112130
if err != nil {
113131
return &fileHandle{
114-
modTime: modTime,
132+
modTime: fi.ModTime(),
133+
size: fi.Size(),
115134
err: err,
116135
}, nil
117136
}
118137
return &fileHandle{
119-
modTime: modTime,
138+
modTime: fi.ModTime(),
139+
size: fi.Size(),
120140
uri: uri,
121141
bytes: data,
122142
hash: hashContents(data),

internal/lsp/cache/workspace_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (s *osFileSource) GetFile(ctx context.Context, uri span.URI) (source.FileHa
8181
uri: uri,
8282
}, nil
8383
}
84-
fh, err := readFile(ctx, uri, fi.ModTime())
84+
fh, err := readFile(ctx, uri, fi)
8585
if err != nil {
8686
return nil, err
8787
}

0 commit comments

Comments
 (0)