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 }