]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go/internal/cache: implement build artifact cache
authorRuss Cox <rsc@golang.org>
Sat, 19 Aug 2017 04:20:00 +0000 (00:20 -0400)
committerRuss Cox <rsc@golang.org>
Thu, 2 Nov 2017 03:32:31 +0000 (03:32 +0000)
The cache is stored in $GOCACHE, which is printed by go env and
defaults to a subdirectory named "go-build" in the standard user cache
directory for the host operating system.

This CL only implements the cache. Future CLs will store data in it.

Change-Id: I0b4965a9e50f852e17e44ec3d6dafe05b58f0d22
Reviewed-on: https://go-review.googlesource.com/68116
Run-TryBot: Russ Cox <rsc@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: David Crawshaw <crawshaw@golang.org>
src/cmd/dist/deps.go
src/cmd/go/internal/cache/cache.go
src/cmd/go/internal/cache/cache_test.go [new file with mode: 0644]
src/cmd/go/internal/cache/default.go [new file with mode: 0644]
src/cmd/go/internal/cache/hash.go
src/cmd/go/internal/cache/hash_test.go
src/cmd/go/internal/work/action.go
src/cmd/go/internal/work/buildid.go

index 969cb07f7c6e4b1e5242360d5f42244adceac964..eef21c9a86ff390a10c7dc154a8606d022b1b66b 100644 (file)
@@ -85,11 +85,20 @@ var builddeps = map[string][]string{
        },
 
        "cmd/go/internal/cache": {
-               "crypto/sha256", // cmd/go/internal/cache
-               "fmt",           // cmd/go/internal/cache
-               "hash",          // cmd/go/internal/cache
-               "io",            // cmd/go/internal/cache
-               "os",            // cmd/go/internal/cache
+               "bytes",                // cmd/go/internal/cache
+               "cmd/go/internal/base", // cmd/go/internal/cache
+               "crypto/sha256",        // cmd/go/internal/cache
+               "encoding/hex",         // cmd/go/internal/cache
+               "errors",               // cmd/go/internal/cache
+               "fmt",                  // cmd/go/internal/cache
+               "hash",                 // cmd/go/internal/cache
+               "io",                   // cmd/go/internal/cache
+               "io/ioutil",            // cmd/go/internal/cache
+               "os",                   // cmd/go/internal/cache
+               "path/filepath",        // cmd/go/internal/cache
+               "runtime",              // cmd/go/internal/cache
+               "strconv",              // cmd/go/internal/cache
+               "sync",                 // cmd/go/internal/cache
        },
 
        "cmd/go/internal/cfg": {
@@ -483,6 +492,13 @@ var builddeps = map[string][]string{
                "reflect", // encoding/binary
        },
 
+       "encoding/hex": {
+               "bytes",  // encoding/hex
+               "errors", // encoding/hex
+               "fmt",    // encoding/hex
+               "io",     // encoding/hex
+       },
+
        "encoding/json": {
                "bytes",           // encoding/json
                "encoding",        // encoding/json
index 97afe959fcf80f1431e0cc89b9e09b113c4a10ef..e908aaec557c39e5d9a884954ba0cd97d87d7fe4 100644 (file)
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package cache implements a package cache,
-// or more properly a build artifact cache,
-// but our only cached build artifacts are packages.
+// Package cache implements a build artifact cache.
 package cache
 
+import (
+       "bytes"
+       "crypto/sha256"
+       "encoding/hex"
+       "errors"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "strconv"
+)
+
 // An ActionID is a cache action key, the hash of a complete description of a
 // repeatable computation (command line, environment variables,
 // input file contents, executable contents).
 type ActionID [HashSize]byte
+
+// An OutputID is a cache output key, the hash of an output of a computation.
+type OutputID [HashSize]byte
+
+// A Cache is a package cache, backed by a file system directory tree.
+type Cache struct {
+       dir string
+}
+
+// Open opens and returns the cache in the given directory.
+//
+// It is safe for multiple processes on a single machine to use the
+// same cache directory in a local file system simultaneously.
+// They will coordinate using operating system file locks and may
+// duplicate effort but will not corrupt the cache.
+//
+// However, it is NOT safe for multiple processes on different machines
+// to share a cache directory (for example, if the directory were stored
+// in a network file system). File locking is notoriously unreliable in
+// network file systems and may not suffice to protect the cache.
+//
+func Open(dir string) (*Cache, error) {
+       info, err := os.Stat(dir)
+       if err != nil {
+               return nil, err
+       }
+       if !info.IsDir() {
+               return nil, &os.PathError{Op: "open", Path: dir, Err: fmt.Errorf("not a directory")}
+       }
+       for i := 0; i < 256; i++ {
+               name := filepath.Join(dir, fmt.Sprintf("%02x", i))
+               if err := os.MkdirAll(name, 0777); err != nil {
+                       return nil, err
+               }
+       }
+       c := &Cache{dir: dir}
+       return c, nil
+}
+
+// fileName returns the name of the file corresponding to the given id.
+func (c *Cache) fileName(id [HashSize]byte, key string) string {
+       return filepath.Join(c.dir, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+"-"+key)
+}
+
+var errMissing = errors.New("cache entry not found")
+
+const (
+       // action entry file is "v1 <hex id> <hex out> <decimal size space-padded to 20 bytes>\n"
+       hexSize   = HashSize * 2
+       entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1
+)
+
+// Get looks up the action ID in the cache,
+// returning the corresponding output ID and file size, if any.
+// Note that finding an output ID does not guarantee that the
+// saved file for that output ID is still available.
+func (c *Cache) Get(id ActionID) (OutputID, int64, error) {
+       missing := func() (OutputID, int64, error) {
+               // TODO: log miss
+               return OutputID{}, 0, errMissing
+       }
+       f, err := os.Open(c.fileName(id, "a"))
+       if err != nil {
+               return missing()
+       }
+       defer f.Close()
+       entry := make([]byte, entrySize+1) // +1 to detect whether f is too long
+       if n, err := io.ReadFull(f, entry); n != entrySize || err != io.ErrUnexpectedEOF {
+               return missing()
+       }
+       if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+64] != ' ' || entry[entrySize-1] != '\n' {
+               return missing()
+       }
+       eid, eout, esize := entry[3:3+hexSize], entry[3+hexSize+1:3+hexSize+1+hexSize], entry[3+hexSize+1+hexSize+1:entrySize-1]
+       var buf [HashSize]byte
+       if _, err := hex.Decode(buf[:], eid); err != nil || buf != id {
+               return missing()
+       }
+       if _, err := hex.Decode(buf[:], eout); err != nil {
+               return missing()
+       }
+       i := 0
+       for i < len(esize) && esize[i] == ' ' {
+               i++
+       }
+       size, err := strconv.ParseInt(string(esize[i:]), 10, 64)
+       if err != nil || size < 0 {
+               return missing()
+       }
+
+       // TODO: Update modtime of f to give a signal about recently used?
+       // TODO: log hit
+
+       return buf, size, nil
+}
+
+// OutputFile returns the name of the cache file storing output with the given OutputID.
+func (c *Cache) OutputFile(out OutputID) string {
+       return c.fileName(out, "d")
+}
+
+// putIndexEntry adds an entry to the cache recording that executing the action
+// with the given id produces an output with the given output id (hash) and size.
+func (c *Cache) putIndexEntry(id ActionID, out OutputID, size int64) error {
+       // Note: We expect that for one reason or another it may happen
+       // that repeating an action produces a different output hash
+       // (for example, if the output contains a time stamp or temp dir name).
+       // While not ideal, this is also not a correctness problem, so we
+       // don't make a big deal about it. In particular, we leave the action
+       // cache entries writable specifically so that they can be overwritten.
+       entry := []byte(fmt.Sprintf("v1 %x %x %20d\n", id, out, size))
+       return ioutil.WriteFile(c.fileName(id, "a"), entry, 0666)
+}
+
+// Put stores the given output in the cache as the output for the action ID.
+// It may read file twice. The content of file must not change between the two passes.
+func (c *Cache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
+       // Compute output ID.
+       h := sha256.New()
+       if _, err := file.Seek(0, 0); err != nil {
+               return OutputID{}, 0, err
+       }
+       size, err := io.Copy(h, file)
+       if err != nil {
+               return OutputID{}, 0, err
+       }
+       var out OutputID
+       h.Sum(out[:0])
+
+       // Copy to cached output file (if not already present).
+       if err := c.copyFile(file, out, size); err != nil {
+               return out, size, err
+       }
+
+       // Add to cache index.
+       // TODO: log put
+       return out, size, c.putIndexEntry(id, out, size)
+}
+
+// copyFile copies file into the cache, expecting it to have the given
+// output ID and size, if that file is not present already.
+func (c *Cache) copyFile(file io.ReadSeeker, out OutputID, size int64) error {
+       name := c.fileName(out, "d")
+       info, err := os.Stat(name)
+       if err == nil && info.Size() == size {
+               // Check hash.
+               if f, err := os.Open(name); err == nil {
+                       h := sha256.New()
+                       io.Copy(h, f)
+                       f.Close()
+                       var out2 OutputID
+                       h.Sum(out2[:0])
+                       if out == out2 {
+                               return nil
+                       }
+               }
+               // Hash did not match. Fall through and rewrite file.
+       }
+
+       // Copy file to cache directory.
+       mode := os.O_RDWR | os.O_CREATE
+       if err == nil && info.Size() > size { // shouldn't happen but fix in case
+               mode |= os.O_TRUNC
+       }
+       f, err := os.OpenFile(name, mode, 0666)
+       if err != nil {
+               return err
+       }
+       defer f.Close()
+       if size == 0 {
+               // File now exists with correct size.
+               // Only one possible zero-length file, so contents are OK too.
+               // Early return here makes sure there's a "last byte" for code below.
+               return nil
+       }
+
+       // From here on, if any of the I/O writing the file fails,
+       // we make a best-effort attempt to truncate the file f
+       // before returning, to avoid leaving bad bytes in the file.
+
+       // Copy file to f, but also into h to double-check hash.
+       if _, err := file.Seek(0, 0); err != nil {
+               f.Truncate(0)
+               return err
+       }
+       h := sha256.New()
+       w := io.MultiWriter(f, h)
+       if _, err := io.CopyN(w, file, size-1); err != nil {
+               f.Truncate(0)
+               return err
+       }
+       // Check last byte before writing it; writing it will make the size match
+       // what other processes expect to find and might cause them to start
+       // using the file.
+       buf := make([]byte, 1)
+       if _, err := file.Read(buf); err != nil {
+               f.Truncate(0)
+               return err
+       }
+       h.Write(buf)
+       sum := h.Sum(nil)
+       if !bytes.Equal(sum, out[:]) {
+               f.Truncate(0)
+               return fmt.Errorf("file content changed underfoot")
+       }
+
+       // Commit cache file entry.
+       if _, err := f.Write(buf); err != nil {
+               f.Truncate(0)
+               return err
+       }
+       if err := f.Close(); err != nil {
+               // Data might not have been written,
+               // but file may look like it is the right size.
+               // To be extra careful, remove cached file.
+               os.Remove(name)
+               return err
+       }
+
+       return nil
+}
diff --git a/src/cmd/go/internal/cache/cache_test.go b/src/cmd/go/internal/cache/cache_test.go
new file mode 100644 (file)
index 0000000..773698c
--- /dev/null
@@ -0,0 +1,106 @@
+// Copyright 2017 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.
+
+package cache
+
+import (
+       "encoding/binary"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "testing"
+)
+
+func TestBasic(t *testing.T) {
+       dir, err := ioutil.TempDir("", "cachetest-")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(dir)
+       _, err = Open(filepath.Join(dir, "notexist"))
+       if err == nil {
+               t.Fatal(`Open("tmp/notexist") succeeded, want failure`)
+       }
+
+       cdir := filepath.Join(dir, "c1")
+       if err := os.Mkdir(cdir, 0777); err != nil {
+               t.Fatal(err)
+       }
+
+       c1, err := Open(cdir)
+       if err != nil {
+               t.Fatalf("Open(c1) (create): %v", err)
+       }
+       if err := c1.putIndexEntry(dummyID(1), dummyID(12), 13); err != nil {
+               t.Fatalf("addIndexEntry: %v", err)
+       }
+       if err := c1.putIndexEntry(dummyID(1), dummyID(2), 3); err != nil { // overwrite entry
+               t.Fatalf("addIndexEntry: %v", err)
+       }
+       if out, size, err := c1.Get(dummyID(1)); err != nil || out != dummyID(2) || size != 3 {
+               t.Fatalf("c1.Get(1) = %x, %v, %v, want %x, %v, nil", out[:], size, err, dummyID(2), 3)
+       }
+
+       c2, err := Open(cdir)
+       if err != nil {
+               t.Fatalf("Open(c2) (reuse): %v", err)
+       }
+       if out, size, err := c2.Get(dummyID(1)); err != nil || out != dummyID(2) || size != 3 {
+               t.Fatalf("c2.Get(1) = %x, %v, %v, want %x, %v, nil", out[:], size, err, dummyID(2), 3)
+       }
+       if err := c2.putIndexEntry(dummyID(2), dummyID(3), 4); err != nil {
+               t.Fatalf("addIndexEntry: %v", err)
+       }
+       if out, size, err := c1.Get(dummyID(2)); err != nil || out != dummyID(3) || size != 4 {
+               t.Fatalf("c1.Get(2) = %x, %v, %v, want %x, %v, nil", out[:], size, err, dummyID(3), 4)
+       }
+}
+
+func TestGrowth(t *testing.T) {
+       dir, err := ioutil.TempDir("", "cachetest-")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer os.RemoveAll(dir)
+
+       c, err := Open(dir)
+       if err != nil {
+               t.Fatalf("Open: %v", err)
+       }
+
+       n := 10000
+       if testing.Short() {
+               n = 1000
+       }
+
+       for i := 0; i < n; i++ {
+               if err := c.putIndexEntry(dummyID(i), dummyID(i*99), int64(i)*101); err != nil {
+                       t.Fatalf("addIndexEntry: %v", err)
+               }
+               id := ActionID(dummyID(i))
+               out, size, err := c.Get(id)
+               if err != nil {
+                       t.Fatalf("Get(%x): %v", id, err)
+               }
+               if out != dummyID(i*99) || size != int64(i)*101 {
+                       t.Errorf("Get(%x) = %x, %d, want %x, %d", id, out, size, dummyID(i*99), int64(i)*101)
+               }
+       }
+       for i := 0; i < n; i++ {
+               id := ActionID(dummyID(i))
+               out, size, err := c.Get(id)
+               if err != nil {
+                       t.Fatalf("Get2(%x): %v", id, err)
+               }
+               if out != dummyID(i*99) || size != int64(i)*101 {
+                       t.Errorf("Get2(%x) = %x, %d, want %x, %d", id, out, size, dummyID(i*99), int64(i)*101)
+               }
+       }
+}
+
+func dummyID(x int) [HashSize]byte {
+       var out [HashSize]byte
+       binary.LittleEndian.PutUint64(out[:], uint64(x))
+       return out
+}
diff --git a/src/cmd/go/internal/cache/default.go b/src/cmd/go/internal/cache/default.go
new file mode 100644 (file)
index 0000000..65b95a3
--- /dev/null
@@ -0,0 +1,77 @@
+// Copyright 2017 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.
+
+package cache
+
+import (
+       "cmd/go/internal/base"
+       "os"
+       "path/filepath"
+       "runtime"
+       "sync"
+)
+
+// Default returns the default cache to use, or nil if no cache should be used.
+func Default() *Cache {
+       defaultOnce.Do(initDefaultCache)
+       return defaultCache
+}
+
+var (
+       defaultOnce  sync.Once
+       defaultCache *Cache
+)
+
+// initDefaultCache does the work of finding the default cache
+// the first time Default is called.
+func initDefaultCache() {
+       dir := os.Getenv("GOCACHE")
+       if dir == "off" {
+               return
+       }
+       if dir == "" {
+               // Compute default location.
+               // TODO(rsc): This code belongs somewhere else,
+               // like maybe ioutil.CacheDir or os.CacheDir.
+               switch runtime.GOOS {
+               case "windows":
+                       dir = os.Getenv("LocalAppData")
+
+               case "darwin":
+                       dir = os.Getenv("HOME")
+                       if dir == "" {
+                               return
+                       }
+                       dir += "/Library/Caches"
+
+               case "plan9":
+                       dir = os.Getenv("home")
+                       if dir == "" {
+                               return
+                       }
+                       dir += "/lib/cache"
+
+               default: // Unix
+                       // https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
+                       dir = os.Getenv("XDG_CACHE_HOME")
+                       if dir == "" {
+                               dir = os.Getenv("HOME")
+                               if dir == "" {
+                                       return
+                               }
+                               dir += "/.cache"
+                       }
+               }
+               dir = filepath.Join(dir, "go-build")
+               if err := os.MkdirAll(dir, 0777); err != nil {
+                       return
+               }
+       }
+
+       c, err := Open(dir)
+       if err != nil {
+               base.Fatalf("initializing cache in $GOCACHE: %s", err)
+       }
+       defaultCache = c
+}
index 7f7261fb64267287820517d502ab22b452ba86fd..b8896aa2f95a1b019baa7bdcdd8215ec59596a2d 100644 (file)
@@ -10,6 +10,7 @@ import (
        "hash"
        "io"
        "os"
+       "sync"
 )
 
 var debugHash = os.Getenv("GOCMDDEBUGHASH") == "1"
@@ -52,8 +53,24 @@ func (h *Hash) Sum() [HashSize]byte {
        return out
 }
 
+var hashFileCache struct {
+       sync.Mutex
+       m map[string][HashSize]byte
+}
+
 // HashFile returns the hash of the named file.
-func HashFile(file string) ([HashSize]byte, error) {
+// It caches repeated lookups for a given file,
+// and the cache entry for a file can be initialized
+// using SetFileHash.
+func FileHash(file string) ([HashSize]byte, error) {
+       hashFileCache.Lock()
+       out, ok := hashFileCache.m[file]
+       hashFileCache.Unlock()
+
+       if ok {
+               return out, nil
+       }
+
        h := sha256.New()
        f, err := os.Open(file)
        if err != nil {
@@ -62,12 +79,29 @@ func HashFile(file string) ([HashSize]byte, error) {
                }
                return [HashSize]byte{}, err
        }
-       io.Copy(h, f)
+       _, err = io.Copy(h, f)
        f.Close()
-       var out [HashSize]byte
+       if err != nil {
+               if debugHash {
+                       fmt.Fprintf(os.Stderr, "HASH %s: %v\n", file, err)
+               }
+               return [HashSize]byte{}, err
+       }
        h.Sum(out[:0])
        if debugHash {
                fmt.Fprintf(os.Stderr, "HASH %s: %x\n", file, out)
        }
+
+       SetFileHash(file, out)
        return out, nil
 }
+
+// SetFileHash sets the hash returned by FileHash for file.
+func SetFileHash(file string, sum [HashSize]byte) {
+       hashFileCache.Lock()
+       if hashFileCache.m == nil {
+               hashFileCache.m = make(map[string][HashSize]byte)
+       }
+       hashFileCache.m[file] = sum
+       hashFileCache.Unlock()
+}
index 493d39339feba89ca57c6fec54fe82b198a9fa1c..312380f6e2e9e4e25fd83567db15f86b98e95ce4 100644 (file)
@@ -34,7 +34,7 @@ func TestHashFile(t *testing.T) {
        }
 
        var h ActionID // make sure hash result is assignable to ActionID
-       h, err = HashFile(name)
+       h, err = FileHash(name)
        if err != nil {
                t.Fatal(err)
        }
index 413e950d6e71f488558fcb316050ab96410113ab..7fbb8b5fba8ce3125615adfd709643b9524b508f 100644 (file)
@@ -47,10 +47,9 @@ type Builder struct {
        readySema chan bool
        ready     actionQueue
 
-       id            sync.Mutex
-       toolIDCache   map[string]string // tool name -> tool ID
-       buildIDCache  map[string]string // file name -> build ID
-       fileHashCache map[string]string // file name -> content hash
+       id           sync.Mutex
+       toolIDCache  map[string]string // tool name -> tool ID
+       buildIDCache map[string]string // file name -> build ID
 }
 
 // NOTE: Much of Action would not need to be exported if not for test.
@@ -195,7 +194,6 @@ func (b *Builder) Init() {
        b.mkdirCache = make(map[string]bool)
        b.toolIDCache = make(map[string]string)
        b.buildIDCache = make(map[string]string)
-       b.fileHashCache = make(map[string]string)
 
        if cfg.BuildN {
                b.WorkDir = "$WORK"
index a4bdafc4e2ab89e0324025a268943575ced0a556..1ac7fbc2ddff17328b5508fd9da48d7d53777a3e 100644 (file)
@@ -216,25 +216,11 @@ func (b *Builder) buildID(file string) string {
 
 // fileHash returns the content hash of the named file.
 func (b *Builder) fileHash(file string) string {
-       b.id.Lock()
-       id := b.fileHashCache[file]
-       b.id.Unlock()
-
-       if id != "" {
-               return id
-       }
-
-       sum, err := cache.HashFile(file)
+       sum, err := cache.FileHash(file)
        if err != nil {
                return ""
        }
-       id = hashToString(sum)
-
-       b.id.Lock()
-       b.fileHashCache[file] = id
-       b.id.Unlock()
-
-       return id
+       return hashToString(sum)
 }
 
 // useCache tries to satisfy the action a, which has action ID actionHash,