-
Notifications
You must be signed in to change notification settings - Fork 0
/
filecache.go
608 lines (557 loc) · 19.9 KB
/
filecache.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The filecache package provides a file-based shared durable blob cache.
//
// The cache is a machine-global mapping from (kind string, key
// [32]byte) to []byte, where kind is an identifier describing the
// namespace or purpose (e.g. "analysis"), and key is a SHA-256 digest
// of the recipe of the value. (It need not be the digest of the value
// itself, so you can query the cache without knowing what value the
// recipe would produce.)
//
// The space budget of the cache can be controlled by [SetBudget].
// Cache entries may be evicted at any time or in any order.
// Note that "du -sh $GOPLSCACHE" may report a disk usage
// figure that is rather larger (e.g. 50%) than the budget because
// it rounds up partial disk blocks.
//
// The Get and Set operations are concurrency-safe.
package filecache
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"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 is automatically called by the first call to Get, but may be called
// explicitly to pre-initialize the cache.
func Start() {
go getCacheDir()
}
// As an optimization, use a 100MB in-memory LRU cache in front of filecache
// operations. This reduces I/O for operations such as diagnostics or
// implementations that repeatedly access the same cache entries.
var memCache = lru.New(100 * 1e6)
type memKey struct {
kind string
key [32]byte
}
// Get retrieves from the cache and returns the value most recently
// supplied to Set(kind, key), possibly by another process.
// Get returns ErrNotFound if the value was not found.
//
// Callers should not modify the returned array.
func Get(kind string, key [32]byte) ([]byte, error) {
// First consult the read-through memory cache.
// Note that memory cache hits do not update the times
// used for LRU eviction of the file-based cache.
if value := memCache.Get(memKey{kind, key}); value != nil {
return value.([]byte), nil
}
iolimit <- struct{}{} // acquire a token
defer func() { <-iolimit }() // release a token
// Read the index file, which provides the name of the CAS file.
indexName, err := filename(kind, key)
if err != nil {
return nil, err
}
indexData, err := os.ReadFile(indexName)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, ErrNotFound
}
return nil, err
}
var valueHash [32]byte
if copy(valueHash[:], indexData) != len(valueHash) {
return nil, ErrNotFound // index entry has wrong length
}
// Read the CAS file and check its contents match.
//
// This ensures integrity in all cases (corrupt or truncated
// file, short read, I/O error, wrong length, etc) except an
// engineered hash collision, which is infeasible.
casName, err := filename(casKind, valueHash)
if err != nil {
return nil, err
}
value, _ := os.ReadFile(casName) // ignore error
if sha256.Sum256(value) != valueHash {
return nil, ErrNotFound // CAS file is missing or has wrong contents
}
// Update file times used by LRU eviction.
//
// Because this turns a read into a write operation,
// we follow the approach used in the go command's
// cache and update the access time only if the
// existing timestamp is older than one hour.
//
// (Traditionally the access time would be updated
// automatically, but for efficiency most POSIX systems have
// for many years set the noatime mount option to avoid every
// open or read operation entailing a metadata write.)
now := time.Now()
touch := func(filename string) {
st, err := os.Stat(filename)
if err == nil && now.Sub(st.ModTime()) > time.Hour {
os.Chtimes(filename, now, now) // ignore error
}
}
touch(indexName)
touch(casName)
memCache.Set(memKey{kind, key}, value, len(value))
return value, nil
}
// ErrNotFound is the distinguished error
// returned by Get when the key is not found.
var ErrNotFound = fmt.Errorf("not found")
// Set updates the value in the cache.
func Set(kind string, key [32]byte, value []byte) error {
memCache.Set(memKey{kind, key}, value, len(value))
// Set the active event to wake up the GC.
select {
case active <- struct{}{}:
default:
}
iolimit <- struct{}{} // acquire a token
defer func() { <-iolimit }() // release a token
// First, add the value to the content-
// addressable store (CAS), if not present.
hash := sha256.Sum256(value)
casName, err := filename(casKind, hash)
if err != nil {
return err
}
// Does CAS file exist and have correct (complete) content?
// TODO(adonovan): opt: use mmap for this check.
if prev, _ := os.ReadFile(casName); !bytes.Equal(prev, value) {
if err := os.MkdirAll(filepath.Dir(casName), 0700); err != nil {
return err
}
// Avoiding O_TRUNC here is merely an optimization to avoid
// cache misses when two threads race to write the same file.
if err := writeFileNoTrunc(casName, value, 0600); err != nil {
os.Remove(casName) // ignore error
return err // e.g. disk full
}
}
// Now write an index entry that refers to the CAS file.
indexName, err := filename(kind, key)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(indexName), 0700); err != nil {
return err
}
if err := writeFileNoTrunc(indexName, hash[:], 0600); err != nil {
os.Remove(indexName) // ignore error
return err // e.g. disk full
}
return nil
}
// The active 1-channel is a selectable resettable event
// indicating recent cache activity.
var active = make(chan struct{}, 1)
// writeFileNoTrunc is like os.WriteFile but doesn't truncate until
// after the write, so that racing writes of the same data are idempotent.
func writeFileNoTrunc(filename string, data []byte, perm os.FileMode) error {
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err == nil {
err = f.Truncate(int64(len(data)))
}
if closeErr := f.Close(); err == nil {
err = closeErr
}
return err
}
// reserved kind strings
const (
casKind = "cas" // content-addressable store files
bugKind = "bug" // gopls bug reports
)
var iolimit = make(chan struct{}, 128) // counting semaphore to limit I/O concurrency in Set.
var budget int64 = 1e9 // 1GB
// SetBudget sets a soft limit on disk usage of regular files in the
// cache (in bytes) and returns the previous value. Supplying a
// negative value queries the current value without changing it.
//
// If two gopls processes have different budgets, the one with the
// lower budget will collect garbage more actively, but both will
// observe the effect.
//
// Even in the steady state, the storage usage reported by the 'du'
// command may exceed the budget by as much as a factor of 3 due to
// the overheads of directories and the effects of block quantization,
// which are especially pronounced for the small index files.
func SetBudget(new int64) (old int64) {
if new < 0 {
return atomic.LoadInt64(&budget)
}
return atomic.SwapInt64(&budget, new)
}
// --- implementation ----
// filename returns the name of the cache file of the specified kind and key.
//
// A typical cache file has a name such as:
//
// $HOME/Library/Caches / gopls / VVVVVVVV / KK / KKKK...KKKK - kind
//
// The portions separated by spaces are as follows:
// - The user's preferred cache directory; the default value varies by OS.
// - The constant "gopls".
// - The "version", 32 bits of the digest of the gopls executable.
// - The first 8 bits of the key, to avoid huge directories.
// - The full 256 bits of the key.
// - The kind or purpose of this cache file (e.g. "analysis").
//
// The kind establishes a namespace for the keys. It is represented as
// a suffix, not a segment, as this significantly reduces the number
// of directories created, and thus the storage overhead.
//
// Previous iterations of the design aimed for the invariant that once
// a file is written, its contents are never modified, though it may
// be atomically replaced or removed. However, not all platforms have
// an atomic rename operation (our first approach), and file locking
// (our second) is a notoriously fickle mechanism.
//
// The current design instead exploits a trick from the cache
// implementation used by the go command: writes of small files are in
// practice atomic (all or nothing) on all platforms.
// (See GOROOT/src/cmd/go/internal/cache/cache.go.)
//
// Russ Cox notes: "all file systems use an rwlock around every file
// system block, including data blocks, so any writes or reads within
// the same block are going to be handled atomically by the FS
// implementation without any need to request file locking explicitly.
// And since the files are so small, there's only one block. (A block
// is at minimum 512 bytes, usually much more.)" And: "all modern file
// systems protect against [partial writes due to power loss] with
// journals."
//
// We use a two-level scheme consisting of an index and a
// content-addressable store (CAS). A single cache entry consists of
// two files. The value of a cache entry is written into the file at
// filename("cas", sha256(value)). Since the value may be arbitrarily
// large, this write is not atomic. That means we must check the
// integrity of the contents read back from the CAS to make sure they
// hash to the expected key. If the CAS file is incomplete or
// inconsistent, we proceed as if it were missing.
//
// Once the CAS file has been written, we write a small fixed-size
// index file at filename(kind, key), using the values supplied by the
// caller. The index file contains the hash that identifies the value
// file in the CAS. (We could add extra metadata to this file, up to
// 512B, the minimum size of a disk block, if later desired, so long
// as the total size remains fixed.) Because the index file is small,
// concurrent writes to it are atomic in practice, even though this is
// not guaranteed by any OS. The fixed size ensures that readers can't
// see a palimpsest when a short new file overwrites a longer old one.
//
// New versions of gopls are free to reorganize the contents of the
// version directory as needs evolve. But all versions of gopls must
// in perpetuity treat the "gopls" directory in a common fashion.
//
// In particular, each gopls process attempts to garbage collect
// the entire gopls directory so that newer binaries can clean up
// after older ones: in the development cycle especially, new
// versions may be created frequently.
func filename(kind string, key [32]byte) (string, error) {
base := fmt.Sprintf("%x-%s", key, kind)
dir, err := getCacheDir()
if err != nil {
return "", err
}
// Keep the BugReports function consistent with this one.
return filepath.Join(dir, base[:2], base), nil
}
// getCacheDir returns the persistent cache directory of all processes
// running this version of the gopls executable.
//
// It must incorporate the hash of the executable so that we needn't
// worry about incompatible changes to the file format or changes to
// the algorithm that produced the index.
func getCacheDir() (string, error) {
cacheDirOnce.Do(func() {
// Use user's preferred cache directory.
userDir := os.Getenv("GOPLSCACHE")
if userDir == "" {
var err error
userDir, err = os.UserCacheDir()
if err != nil {
userDir = os.TempDir()
}
}
goplsDir := filepath.Join(userDir, "gopls")
// UserCacheDir may return a nonexistent directory
// (in which case we must create it, which may fail),
// or it may return a non-writable directory, in
// which case we should ideally respect the user's express
// wishes (e.g. XDG_CACHE_HOME) and not write somewhere else.
// Sadly UserCacheDir doesn't currently let us distinguish
// such intent from accidental misconfiguraton such as HOME=/
// in a CI builder. So, we check whether the gopls subdirectory
// can be created (or already exists) and not fall back to /tmp.
// See also https://github.com/golang/go/issues/57638.
if os.MkdirAll(goplsDir, 0700) != nil {
goplsDir = filepath.Join(os.TempDir(), "gopls")
}
// Start the garbage collector.
go gc(goplsDir)
// Compute the hash of this executable (~20ms) and create a subdirectory.
hash, err := hashExecutable()
if err != nil {
cacheDirErr = fmt.Errorf("can't hash gopls executable: %v", err)
}
// Use only 32 bits of the digest to avoid unwieldy filenames.
// It's not an adversarial situation.
cacheDir = filepath.Join(goplsDir, fmt.Sprintf("%x", hash[:4]))
if err := os.MkdirAll(cacheDir, 0700); err != nil {
cacheDirErr = fmt.Errorf("can't create cache: %v", err)
}
})
return cacheDir, cacheDirErr
}
var (
cacheDirOnce sync.Once
cacheDir string
cacheDirErr error
)
func hashExecutable() (hash [32]byte, err error) {
exe, err := os.Executable()
if err != nil {
return hash, err
}
f, err := os.Open(exe)
if err != nil {
return hash, err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return hash, fmt.Errorf("can't read executable: %w", err)
}
h.Sum(hash[:0])
return hash, nil
}
// gc runs forever, periodically deleting files from the gopls
// directory until the space budget is no longer exceeded, and also
// deleting files older than the maximum age, regardless of budget.
//
// One gopls process may delete garbage created by a different gopls
// process, possibly running a different version of gopls, possibly
// running concurrently.
func gc(goplsDir string) {
// period between collections
//
// Originally the period was always 1 minute, but this
// consumed 15% of a CPU core when idle (#61049).
//
// The reason for running collections even when idle is so
// that long lived gopls sessions eventually clean up the
// caches created by defunct executables.
const (
minPeriod = 5 * time.Minute // when active
maxPeriod = 6 * time.Hour // when idle
)
// Sleep statDelay*batchSize between stats to smooth out I/O.
//
// The constants below were chosen using the following heuristics:
// - 1GB of filecache is on the order of ~100-200k files, in which case
// 100μs delay per file introduces 10-20s of additional walk time,
// less than the minPeriod.
// - Processing batches of stats at once is much more efficient than
// sleeping after every stat (due to OS optimizations).
const statDelay = 100 * time.Microsecond // average delay between stats, to smooth out I/O
const batchSize = 1000 // # of stats to process before sleeping
const maxAge = 5 * 24 * time.Hour // max time since last access before file is deleted
// The macOS filesystem is strikingly slow, at least on some machines.
// /usr/bin/find achieves only about 25,000 stats per second
// at full speed (no pause between items), meaning a large
// cache may take several minutes to scan.
// We must ensure that short-lived processes (crucially,
// tests) are able to make progress sweeping garbage.
//
// (gopls' caches should never actually get this big in
// practice: the example mentioned above resulted from a bug
// that caused filecache to fail to delete any files.)
const debug = false
// Names of all directories found in first pass; nil thereafter.
dirs := make(map[string]bool)
for {
// Enumerate all files in the cache.
type item struct {
path string
mtime time.Time
size int64
}
var files []item
start := time.Now()
var total int64 // bytes
_ = filepath.Walk(goplsDir, func(path string, stat os.FileInfo, err error) error {
if err != nil {
return nil // ignore errors
}
if stat.IsDir() {
// Collect (potentially empty) directories.
if dirs != nil {
dirs[path] = true
}
} else {
// Unconditionally delete files we haven't used in ages.
// (We do this here, not in the second loop, so that we
// perform age-based collection even in short-lived processes.)
age := time.Since(stat.ModTime())
if age > maxAge {
if debug {
log.Printf("age: deleting stale file %s (%dB, age %v)",
path, stat.Size(), age)
}
os.Remove(path) // ignore error
} else {
files = append(files, item{path, stat.ModTime(), stat.Size()})
total += stat.Size()
if debug && len(files)%1000 == 0 {
log.Printf("filecache: checked %d files in %v", len(files), time.Since(start))
}
if len(files)%batchSize == 0 {
time.Sleep(batchSize * statDelay)
}
}
}
return nil
})
// Sort oldest files first.
sort.Slice(files, func(i, j int) bool {
return files[i].mtime.Before(files[j].mtime)
})
// Delete oldest files until we're under budget.
budget := atomic.LoadInt64(&budget)
for _, file := range files {
if total < budget {
break
}
if debug {
age := time.Since(file.mtime)
log.Printf("budget: deleting stale file %s (%dB, age %v)",
file.path, file.size, age)
}
os.Remove(file.path) // ignore error
total -= file.size
}
files = nil // release memory before sleep
// Wait unconditionally for the minimum period.
time.Sleep(minPeriod)
// Once only, delete all directories.
// This will succeed only for the empty ones,
// and ensures that stale directories (whose
// files have been deleted) are removed eventually.
// They don't take up much space but they do slow
// down the traversal.
//
// We do this after the sleep to minimize the
// race against Set, which may create a directory
// that is momentarily empty.
//
// (Test processes don't live that long, so
// this may not be reached on the CI builders.)
if dirs != nil {
dirnames := make([]string, 0, len(dirs))
for dir := range dirs {
dirnames = append(dirnames, dir)
}
dirs = nil
// Descending length order => children before parents.
sort.Slice(dirnames, func(i, j int) bool {
return len(dirnames[i]) > len(dirnames[j])
})
var deleted int
for _, dir := range dirnames {
if os.Remove(dir) == nil { // ignore error
deleted++
}
}
if debug {
log.Printf("deleted %d empty directories", deleted)
}
}
// Wait up to the max period,
// or for Set activity in this process.
select {
case <-active:
case <-time.After(maxPeriod):
}
}
}
func init() {
// Register a handler to durably record this process's first
// assertion failure in the cache so that we can ask users to
// share this information via the stats command.
bug.Handle(func(bug bug.Bug) {
// Wait for cache init (bugs in tests happen early).
_, _ = getCacheDir()
data, err := json.Marshal(bug)
if err != nil {
panic(fmt.Sprintf("error marshalling bug %+v: %v", bug, err))
}
key := sha256.Sum256(data)
_ = Set(bugKind, key, data)
})
}
// BugReports returns a new unordered array of the contents
// of all cached bug reports produced by this executable.
// It also returns the location of the cache directory
// used by this process (or "" on initialization error).
func BugReports() (string, []bug.Bug) {
// To test this logic, run:
// $ TEST_GOPLS_BUG=oops gopls bug # trigger a bug
// $ gopls stats # list the bugs
dir, err := getCacheDir()
if err != nil {
return "", nil // ignore initialization errors
}
var result []bug.Bug
_ = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return nil // ignore readdir/stat errors
}
// Parse the key from each "XXXX-bug" cache file name.
if !info.IsDir() && strings.HasSuffix(path, bugKind) {
var key [32]byte
n, err := hex.Decode(key[:], []byte(filepath.Base(path)[:len(key)*2]))
if err != nil || n != len(key) {
return nil // ignore malformed file names
}
content, err := Get(bugKind, key)
if err == nil { // ignore read errors
var b bug.Bug
if err := json.Unmarshal(content, &b); err != nil {
log.Printf("error marshalling bug %q: %v", string(content), err)
}
result = append(result, b)
}
}
return nil
})
return dir, result
}