src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

cache.go (18333B)


      1 // Copyright 2017 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 // Package cache implements a build artifact cache.
      6 //
      7 // This package is a slightly modified fork of Go's
      8 // cmd/go/internal/cache package.
      9 package cache
     10 
     11 import (
     12 	"bytes"
     13 	"crypto/sha256"
     14 	"encoding/hex"
     15 	"errors"
     16 	"fmt"
     17 	"io"
     18 	"os"
     19 	"path/filepath"
     20 	"strconv"
     21 	"strings"
     22 	"time"
     23 
     24 	"honnef.co/go/tools/internal/renameio"
     25 )
     26 
     27 // An ActionID is a cache action key, the hash of a complete description of a
     28 // repeatable computation (command line, environment variables,
     29 // input file contents, executable contents).
     30 type ActionID [HashSize]byte
     31 
     32 // An OutputID is a cache output key, the hash of an output of a computation.
     33 type OutputID [HashSize]byte
     34 
     35 // Cache is the interface for a build artifact cache.
     36 type Cache interface {
     37 	// Get returns the cache entry for the provided ActionID.
     38 	// On miss, the error type should be of type *entryNotFoundError.
     39 	Get(ActionID) (Entry, error)
     40 
     41 	// Put adds an item to the cache. The seeker is only used to seek to
     42 	// the beginning. After a call to Put, the seek position is not
     43 	// guaranteed to be in any particular state.
     44 	//
     45 	// As a special case, if the ReadSeeker is of type noVerifyReadSeeker,
     46 	// the verification from GODEBUG=gocacheverify=1 is skipped.
     47 	Put(ActionID, io.ReadSeeker) (OutputID, int64, error)
     48 
     49 	// Close closes the cache and releases any resources. For DiskCache
     50 	// this trims old entries.
     51 	Close() error
     52 
     53 	// OutputFile returns the path to the on-disk file for the given OutputID.
     54 	OutputFile(OutputID) string
     55 }
     56 
     57 // A DiskCache is a package cache backed by a file system directory tree.
     58 type DiskCache struct {
     59 	dir string
     60 	now func() time.Time
     61 }
     62 
     63 // Open opens and returns the cache in the given directory.
     64 //
     65 // It is safe for multiple processes on a single machine to use the
     66 // same cache directory in a local file system simultaneously.
     67 // They will coordinate using operating system file locks and may
     68 // duplicate effort but will not corrupt the cache.
     69 //
     70 // However, it is NOT safe for multiple processes on different machines
     71 // to share a cache directory (for example, if the directory were stored
     72 // in a network file system). File locking is notoriously unreliable in
     73 // network file systems and may not suffice to protect the cache.
     74 func Open(dir string) (*DiskCache, error) {
     75 	info, err := os.Stat(dir)
     76 	if err != nil {
     77 		return nil, err
     78 	}
     79 	if !info.IsDir() {
     80 		return nil, &os.PathError{Op: "open", Path: dir, Err: fmt.Errorf("not a directory")}
     81 	}
     82 	for i := range 256 {
     83 		name := filepath.Join(dir, fmt.Sprintf("%02x", i))
     84 		if err := os.MkdirAll(name, 0777); err != nil {
     85 			return nil, err
     86 		}
     87 	}
     88 	c := &DiskCache{
     89 		dir: dir,
     90 		now: time.Now,
     91 	}
     92 	return c, nil
     93 }
     94 
     95 // fileName returns the name of the file corresponding to the given id.
     96 func (c *DiskCache) fileName(id [HashSize]byte, key string) string {
     97 	return filepath.Join(c.dir, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+"-"+key)
     98 }
     99 
    100 // An entryNotFoundError indicates that a cache entry was not found, with an
    101 // optional underlying reason.
    102 type entryNotFoundError struct {
    103 	Err error
    104 }
    105 
    106 func (e *entryNotFoundError) Error() string {
    107 	if e.Err == nil {
    108 		return "cache entry not found"
    109 	}
    110 	return fmt.Sprintf("cache entry not found: %v", e.Err)
    111 }
    112 
    113 func (e *entryNotFoundError) Unwrap() error {
    114 	return e.Err
    115 }
    116 
    117 const (
    118 	// action entry file is "v1 <hex id> <hex out> <decimal size space-padded to 20 bytes> <unixnano space-padded to 20 bytes>\n"
    119 	hexSize   = HashSize * 2
    120 	entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1 + 20 + 1
    121 )
    122 
    123 // verify controls whether to run the cache in verify mode.
    124 // In verify mode, the cache always returns errMissing from Get
    125 // but then double-checks in Put that the data being written
    126 // exactly matches any existing entry. This provides an easy
    127 // way to detect program behavior that would have been different
    128 // had the cache entry been returned from Get.
    129 //
    130 // verify is enabled by setting the environment variable
    131 // GODEBUG=gocacheverify=1.
    132 var verify = false
    133 
    134 var errVerifyMode = errors.New("gocacheverify=1")
    135 
    136 // DebugTest is set when GODEBUG=gocachetest=1 is in the environment.
    137 var DebugTest = false
    138 
    139 func init() { initEnv() }
    140 
    141 func initEnv() {
    142 	verify = false
    143 	debugHash = false
    144 	debug := strings.SplitSeq(os.Getenv("GODEBUG"), ",")
    145 	for f := range debug {
    146 		if f == "gocacheverify=1" {
    147 			verify = true
    148 		}
    149 		if f == "gocachehash=1" {
    150 			debugHash = true
    151 		}
    152 		if f == "gocachetest=1" {
    153 			DebugTest = true
    154 		}
    155 	}
    156 }
    157 
    158 // Get looks up the action ID in the cache,
    159 // returning the corresponding output ID and file size, if any.
    160 // Note that finding an output ID does not guarantee that the
    161 // saved file for that output ID is still available.
    162 func (c *DiskCache) Get(id ActionID) (Entry, error) {
    163 	if verify {
    164 		return Entry{}, &entryNotFoundError{Err: errVerifyMode}
    165 	}
    166 	return c.get(id)
    167 }
    168 
    169 type Entry struct {
    170 	OutputID OutputID
    171 	Size     int64
    172 	Time     time.Time
    173 }
    174 
    175 // get is Get but does not respect verify mode, so that Put can use it.
    176 func (c *DiskCache) get(id ActionID) (Entry, error) {
    177 	missing := func(reason error) (Entry, error) {
    178 		return Entry{}, &entryNotFoundError{Err: reason}
    179 	}
    180 	f, err := os.Open(c.fileName(id, "a"))
    181 	if err != nil {
    182 		return missing(err)
    183 	}
    184 	defer f.Close()
    185 	entry := make([]byte, entrySize+1) // +1 to detect whether f is too long
    186 	if n, err := io.ReadFull(f, entry); n > entrySize {
    187 		return missing(errors.New("too long"))
    188 	} else if err != io.ErrUnexpectedEOF {
    189 		if err == io.EOF {
    190 			return missing(errors.New("file is empty"))
    191 		}
    192 		return missing(err)
    193 	} else if n < entrySize {
    194 		return missing(errors.New("entry file incomplete"))
    195 	}
    196 	if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+hexSize] != ' ' || entry[3+hexSize+1+hexSize+1+20] != ' ' || entry[entrySize-1] != '\n' {
    197 		return missing(errors.New("invalid header"))
    198 	}
    199 	eid, entry := entry[3:3+hexSize], entry[3+hexSize:]
    200 	eout, entry := entry[1:1+hexSize], entry[1+hexSize:]
    201 	esize, entry := entry[1:1+20], entry[1+20:]
    202 	//lint:ignore SA4006 See https://github.com/dominikh/go-tools/issues/465
    203 	etime, entry := entry[1:1+20], entry[1+20:]
    204 	var buf [HashSize]byte
    205 	if _, err := hex.Decode(buf[:], eid); err != nil {
    206 		return missing(fmt.Errorf("decoding ID: %v", err))
    207 	} else if buf != id {
    208 		return missing(errors.New("mismatched ID"))
    209 	}
    210 	if _, err := hex.Decode(buf[:], eout); err != nil {
    211 		return missing(fmt.Errorf("decoding output ID: %v", err))
    212 	}
    213 	i := 0
    214 	for i < len(esize) && esize[i] == ' ' {
    215 		i++
    216 	}
    217 	size, err := strconv.ParseInt(string(esize[i:]), 10, 64)
    218 	if err != nil {
    219 		return missing(fmt.Errorf("parsing size: %v", err))
    220 	} else if size < 0 {
    221 		return missing(errors.New("negative size"))
    222 	}
    223 	i = 0
    224 	for i < len(etime) && etime[i] == ' ' {
    225 		i++
    226 	}
    227 	tm, err := strconv.ParseInt(string(etime[i:]), 10, 64)
    228 	if err != nil {
    229 		return missing(fmt.Errorf("parsing timestamp: %v", err))
    230 	} else if tm < 0 {
    231 		return missing(errors.New("negative timestamp"))
    232 	}
    233 
    234 	c.used(c.fileName(id, "a"))
    235 
    236 	return Entry{buf, size, time.Unix(0, tm)}, nil
    237 }
    238 
    239 // GetFile looks up the action ID in the cache and returns
    240 // the name of the corresponding data file.
    241 func GetFile(c Cache, id ActionID) (file string, entry Entry, err error) {
    242 	entry, err = c.Get(id)
    243 	if err != nil {
    244 		return "", Entry{}, err
    245 	}
    246 	file = c.OutputFile(entry.OutputID)
    247 	info, err := os.Stat(file)
    248 	if err != nil {
    249 		return "", Entry{}, &entryNotFoundError{Err: err}
    250 	}
    251 	if info.Size() != entry.Size {
    252 		return "", Entry{}, &entryNotFoundError{Err: errors.New("file incomplete")}
    253 	}
    254 	return file, entry, nil
    255 }
    256 
    257 // GetBytes looks up the action ID in the cache and returns
    258 // the corresponding output bytes.
    259 // GetBytes should only be used for data that can be expected to fit in memory.
    260 func GetBytes(c Cache, id ActionID) ([]byte, Entry, error) {
    261 	entry, err := c.Get(id)
    262 	if err != nil {
    263 		return nil, entry, err
    264 	}
    265 	data, _ := os.ReadFile(c.OutputFile(entry.OutputID))
    266 	if sha256.Sum256(data) != entry.OutputID {
    267 		return nil, entry, &entryNotFoundError{Err: errors.New("bad checksum")}
    268 	}
    269 	return data, entry, nil
    270 }
    271 
    272 // OutputFile returns the name of the cache file storing output with the given OutputID.
    273 func (c *DiskCache) OutputFile(out OutputID) string {
    274 	file := c.fileName(out, "d")
    275 	c.used(file)
    276 	return file
    277 }
    278 
    279 // Time constants for cache expiration.
    280 //
    281 // We set the mtime on a cache file on each use, but at most one per mtimeInterval (1 hour),
    282 // to avoid causing many unnecessary inode updates. The mtimes therefore
    283 // roughly reflect "time of last use" but may in fact be older by at most an hour.
    284 //
    285 // We scan the cache for entries to delete at most once per trimInterval (1 day).
    286 //
    287 // When we do scan the cache, we delete entries that have not been used for
    288 // at least trimLimit (5 days). Statistics gathered from a month of usage by
    289 // Go developers found that essentially all reuse of cached entries happened
    290 // within 5 days of the previous reuse. See golang.org/issue/22990.
    291 const (
    292 	mtimeInterval = 1 * time.Hour
    293 	trimInterval  = 24 * time.Hour
    294 	trimLimit     = 5 * 24 * time.Hour
    295 )
    296 
    297 // used makes a best-effort attempt to update mtime on file,
    298 // so that mtime reflects cache access time.
    299 //
    300 // Because the reflection only needs to be approximate,
    301 // and to reduce the amount of disk activity caused by using
    302 // cache entries, used only updates the mtime if the current
    303 // mtime is more than an hour old. This heuristic eliminates
    304 // nearly all of the mtime updates that would otherwise happen,
    305 // while still keeping the mtimes useful for cache trimming.
    306 func (c *DiskCache) used(file string) {
    307 	info, err := os.Stat(file)
    308 	if err == nil && c.now().Sub(info.ModTime()) < mtimeInterval {
    309 		return
    310 	}
    311 	os.Chtimes(file, c.now(), c.now())
    312 }
    313 
    314 // Trim removes old cache entries that are likely not to be reused.
    315 func (c *DiskCache) Trim() {
    316 	now := c.now()
    317 
    318 	// We maintain in dir/trim.txt the time of the last completed cache trim.
    319 	// If the cache has been trimmed recently enough, do nothing.
    320 	// This is the common case.
    321 	data, _ := renameio.ReadFile(filepath.Join(c.dir, "trim.txt"))
    322 	t, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
    323 	if err == nil && now.Sub(time.Unix(t, 0)) < trimInterval {
    324 		return
    325 	}
    326 
    327 	// Trim each of the 256 subdirectories.
    328 	// We subtract an additional mtimeInterval
    329 	// to account for the imprecision of our "last used" mtimes.
    330 	cutoff := now.Add(-trimLimit - mtimeInterval)
    331 	for i := range 256 {
    332 		subdir := filepath.Join(c.dir, fmt.Sprintf("%02x", i))
    333 		c.trimSubdir(subdir, cutoff)
    334 	}
    335 
    336 	// Ignore errors from here: if we don't write the complete timestamp, the
    337 	// cache will appear older than it is, and we'll trim it again next time.
    338 	renameio.WriteFile(filepath.Join(c.dir, "trim.txt"), fmt.Appendf(nil, "%d", now.Unix()), 0666)
    339 }
    340 
    341 // Close trims the cache and releases resources.
    342 func (c *DiskCache) Close() error {
    343 	c.Trim()
    344 	return nil
    345 }
    346 
    347 // trimSubdir trims a single cache subdirectory.
    348 func (c *DiskCache) trimSubdir(subdir string, cutoff time.Time) {
    349 	// Read all directory entries from subdir before removing
    350 	// any files, in case removing files invalidates the file offset
    351 	// in the directory scan. Also, ignore error from f.Readdirnames,
    352 	// because we don't care about reporting the error and we still
    353 	// want to process any entries found before the error.
    354 	f, err := os.Open(subdir)
    355 	if err != nil {
    356 		return
    357 	}
    358 	names, _ := f.Readdirnames(-1)
    359 	f.Close()
    360 
    361 	for _, name := range names {
    362 		// Remove only cache entries (xxxx-a and xxxx-d).
    363 		if !strings.HasSuffix(name, "-a") && !strings.HasSuffix(name, "-d") {
    364 			continue
    365 		}
    366 		entry := filepath.Join(subdir, name)
    367 		info, err := os.Stat(entry)
    368 		if err == nil && info.ModTime().Before(cutoff) {
    369 			os.Remove(entry)
    370 		}
    371 	}
    372 }
    373 
    374 // putIndexEntry adds an entry to the cache recording that executing the action
    375 // with the given id produces an output with the given output id (hash) and size.
    376 func (c *DiskCache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify bool) error {
    377 	// Note: We expect that for one reason or another it may happen
    378 	// that repeating an action produces a different output hash
    379 	// (for example, if the output contains a time stamp or temp dir name).
    380 	// While not ideal, this is also not a correctness problem, so we
    381 	// don't make a big deal about it. In particular, we leave the action
    382 	// cache entries writable specifically so that they can be overwritten.
    383 	//
    384 	// Setting GODEBUG=gocacheverify=1 does make a big deal:
    385 	// in verify mode we are double-checking that the cache entries
    386 	// are entirely reproducible. As just noted, this may be unrealistic
    387 	// in some cases but the check is also useful for shaking out real bugs.
    388 	entry := fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano())
    389 	if verify && allowVerify {
    390 		old, err := c.get(id)
    391 		if err == nil && (old.OutputID != out || old.Size != size) {
    392 			// panic to show stack trace, so we can see what code is generating this cache entry.
    393 			msg := fmt.Sprintf("go: internal cache error: cache verify failed: id=%x changed:<<<\n%s\n>>>\nold: %x %d\nnew: %x %d", id, reverseHash(id), out, size, old.OutputID, old.Size)
    394 			panic(msg)
    395 		}
    396 	}
    397 	file := c.fileName(id, "a")
    398 
    399 	// Copy file to cache directory.
    400 	mode := os.O_WRONLY | os.O_CREATE
    401 	f, err := os.OpenFile(file, mode, 0666)
    402 	if err != nil {
    403 		return err
    404 	}
    405 	_, err = f.WriteString(entry)
    406 	if err == nil {
    407 		// Truncate the file only *after* writing it.
    408 		// (This should be a no-op, but truncate just in case of previous corruption.)
    409 		//
    410 		// This differs from ioutil.WriteFile, which truncates to 0 *before* writing
    411 		// via os.O_TRUNC. Truncating only after writing ensures that a second write
    412 		// of the same content to the same file is idempotent, and does not — even
    413 		// temporarily! — undo the effect of the first write.
    414 		err = f.Truncate(int64(len(entry)))
    415 	}
    416 	if closeErr := f.Close(); err == nil {
    417 		err = closeErr
    418 	}
    419 	if err != nil {
    420 		// TODO(bcmills): This Remove potentially races with another go command writing to file.
    421 		// Can we eliminate it?
    422 		os.Remove(file)
    423 		return err
    424 	}
    425 	os.Chtimes(file, c.now(), c.now()) // mainly for tests
    426 
    427 	return nil
    428 }
    429 
    430 // Put stores the given output in the cache as the output for the action ID.
    431 // It may read file twice. The content of file must not change between the two passes.
    432 func (c *DiskCache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
    433 	// Unwrap noVerifyReadSeeker to determine verify mode.
    434 	allowVerify := true
    435 	if nv, ok := file.(noVerifyReadSeeker); ok {
    436 		allowVerify = false
    437 		file = nv.ReadSeeker
    438 	}
    439 	return c.put(id, file, allowVerify)
    440 }
    441 
    442 // noVerifyReadSeeker is an io.ReadSeeker wrapper that indicates that
    443 // the Put should skip GODEBUG=gocacheverify=1 verification.
    444 type noVerifyReadSeeker struct {
    445 	io.ReadSeeker
    446 }
    447 
    448 // PutNoVerify is like Cache.Put but disables the verify check
    449 // when GODEBUG=gocacheverify=1 is set.
    450 // It is meant for data that is OK to cache but that we expect to vary slightly
    451 // from run to run, like test output containing times and the like.
    452 func PutNoVerify(c Cache, id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
    453 	return c.Put(id, noVerifyReadSeeker{file})
    454 }
    455 
    456 func (c *DiskCache) put(id ActionID, file io.ReadSeeker, allowVerify bool) (OutputID, int64, error) {
    457 	// Compute output ID.
    458 	h := sha256.New()
    459 	if _, err := file.Seek(0, 0); err != nil {
    460 		return OutputID{}, 0, err
    461 	}
    462 	size, err := io.Copy(h, file)
    463 	if err != nil {
    464 		return OutputID{}, 0, err
    465 	}
    466 	var out OutputID
    467 	h.Sum(out[:0])
    468 
    469 	// Copy to cached output file (if not already present).
    470 	if err := c.copyFile(file, out, size); err != nil {
    471 		return out, size, err
    472 	}
    473 
    474 	// Add to cache index.
    475 	return out, size, c.putIndexEntry(id, out, size, allowVerify)
    476 }
    477 
    478 // PutBytes stores the given bytes in the cache as the output for the action ID.
    479 func PutBytes(c Cache, id ActionID, data []byte) error {
    480 	_, _, err := c.Put(id, bytes.NewReader(data))
    481 	return err
    482 }
    483 
    484 // copyFile copies file into the cache, expecting it to have the given
    485 // output ID and size, if that file is not present already.
    486 func (c *DiskCache) copyFile(file io.ReadSeeker, out OutputID, size int64) error {
    487 	name := c.fileName(out, "d")
    488 	info, err := os.Stat(name)
    489 	if err == nil && info.Size() == size {
    490 		// Check hash.
    491 		if f, err := os.Open(name); err == nil {
    492 			h := sha256.New()
    493 			io.Copy(h, f)
    494 			f.Close()
    495 			var out2 OutputID
    496 			h.Sum(out2[:0])
    497 			if out == out2 {
    498 				return nil
    499 			}
    500 		}
    501 		// Hash did not match. Fall through and rewrite file.
    502 	}
    503 
    504 	// Copy file to cache directory.
    505 	mode := os.O_RDWR | os.O_CREATE
    506 	if err == nil && info.Size() > size { // shouldn't happen but fix in case
    507 		mode |= os.O_TRUNC
    508 	}
    509 	f, err := os.OpenFile(name, mode, 0666)
    510 	if err != nil {
    511 		return err
    512 	}
    513 	defer f.Close()
    514 	if size == 0 {
    515 		// File now exists with correct size.
    516 		// Only one possible zero-length file, so contents are OK too.
    517 		// Early return here makes sure there's a "last byte" for code below.
    518 		return nil
    519 	}
    520 
    521 	// From here on, if any of the I/O writing the file fails,
    522 	// we make a best-effort attempt to truncate the file f
    523 	// before returning, to avoid leaving bad bytes in the file.
    524 
    525 	// Copy file to f, but also into h to double-check hash.
    526 	if _, err := file.Seek(0, 0); err != nil {
    527 		f.Truncate(0)
    528 		return err
    529 	}
    530 	h := sha256.New()
    531 	w := io.MultiWriter(f, h)
    532 	if _, err := io.CopyN(w, file, size-1); err != nil {
    533 		f.Truncate(0)
    534 		return err
    535 	}
    536 	// Check last byte before writing it; writing it will make the size match
    537 	// what other processes expect to find and might cause them to start
    538 	// using the file.
    539 	buf := make([]byte, 1)
    540 	if _, err := file.Read(buf); err != nil {
    541 		f.Truncate(0)
    542 		return err
    543 	}
    544 	h.Write(buf)
    545 	sum := h.Sum(nil)
    546 	if !bytes.Equal(sum, out[:]) {
    547 		f.Truncate(0)
    548 		return fmt.Errorf("file content changed underfoot")
    549 	}
    550 
    551 	// Commit cache file entry.
    552 	if _, err := f.Write(buf); err != nil {
    553 		f.Truncate(0)
    554 		return err
    555 	}
    556 	if err := f.Close(); err != nil {
    557 		// Data might not have been written,
    558 		// but file may look like it is the right size.
    559 		// To be extra careful, remove cached file.
    560 		os.Remove(name)
    561 		return err
    562 	}
    563 	os.Chtimes(name, c.now(), c.now()) // mainly for tests
    564 
    565 	return nil
    566 }