golang.org/x/tools/gopls@v0.15.3/internal/filecache/filecache.go (about)

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