Skip to content

Commit

Permalink
cmd/go/internal/modfetch: lock files and directories
Browse files Browse the repository at this point in the history
We employ the following new locking mechanisms:

• Zip files and list files within the module cache are written using
  atomic renames of temporary files, so that GOPROXY servers reading
  from the cache will never serve incomplete content.

• A lock file for each module version guards downloading and extraction of
  (immutable) module contents.

• A lock file alongside each version list (named 'list.lock')
  guards updates to the list.

• A single lock file in the module cache guards updates to all go.sum
  files. The go.sum files themselves are written using an atomic
  rename to ensure that we never accidentally discard existing sums.

Updates #26794

RELNOTE=yes

Change-Id: I16ef8b06ee4bd7b94d0c0a6f5d17e1cecc379076
Reviewed-on: https://go-review.googlesource.com/c/146382
Run-TryBot: Bryan C. Mills <bcmills@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
  • Loading branch information
Bryan C. Mills committed Nov 29, 2018
1 parent ba2e8f6 commit 04e12a5
Show file tree
Hide file tree
Showing 5 changed files with 337 additions and 112 deletions.
16 changes: 1 addition & 15 deletions src/cmd/go/internal/clean/clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,27 +176,13 @@ func runClean(cmd *base.Command, args []string) {
b.Showcmd("", "rm -rf %s", modfetch.PkgMod)
}
if !cfg.BuildN {
if err := removeAll(modfetch.PkgMod); err != nil {
if err := modfetch.RemoveAll(modfetch.PkgMod); err != nil {
base.Errorf("go clean -modcache: %v", err)
}
}
}
}

func removeAll(dir string) error {
// Module cache has 0555 directories; make them writable in order to remove content.
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil // ignore errors walking in file system
}
if info.IsDir() {
os.Chmod(path, 0777)
}
return nil
})
return os.RemoveAll(dir)
}

var cleaned = map[*load.Package]bool{}

// TODO: These are dregs left by Makefile-based builds.
Expand Down
75 changes: 52 additions & 23 deletions src/cmd/go/internal/modfetch/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import (
"strings"

"cmd/go/internal/base"
"cmd/go/internal/lockedfile"
"cmd/go/internal/modfetch/codehost"
"cmd/go/internal/module"
"cmd/go/internal/par"
"cmd/go/internal/renameio"
"cmd/go/internal/semver"
)

Expand Down Expand Up @@ -75,6 +77,37 @@ func DownloadDir(m module.Version) (string, error) {
return filepath.Join(PkgMod, enc+"@"+encVer), nil
}

// lockVersion locks a file within the module cache that guards the downloading
// and extraction of the zipfile for the given module version.
func lockVersion(mod module.Version) (unlock func(), err error) {
path, err := CachePath(mod, "lock")
if err != nil {
return nil, err
}
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
return nil, err
}
return lockedfile.MutexAt(path).Lock()
}

// SideLock locks a file within the module cache that that guards edits to files
// outside the cache, such as go.sum and go.mod files in the user's working
// directory. It returns a function that must be called to unlock the file.
func SideLock() (unlock func()) {
if PkgMod == "" {
base.Fatalf("go: internal error: modfetch.PkgMod not set")
}
path := filepath.Join(PkgMod, "cache", "lock")
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
base.Fatalf("go: failed to create cache directory %s: %v", filepath.Dir(path), err)
}
unlock, err := lockedfile.MutexAt(path).Lock()
if err != nil {
base.Fatalf("go: failed to lock file at %v", path)
}
return unlock
}

// A cachingRepo is a cache around an underlying Repo,
// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
// It is also safe for simultaneous use by multiple goroutines
Expand Down Expand Up @@ -386,7 +419,7 @@ func readDiskStatByHash(path, rev string) (file string, info *RevInfo, err error
// and should ignore it.
var oldVgoPrefix = []byte("//vgo 0.0.")

// readDiskGoMod reads a cached stat result from disk,
// readDiskGoMod reads a cached go.mod file from disk,
// returning the name of the cache file and the result.
// If the read fails, the caller can use
// writeDiskGoMod(file, data) to write a new cache entry.
Expand Down Expand Up @@ -452,22 +485,8 @@ func writeDiskCache(file string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
return err
}
// Write data to temp file next to target file.
f, err := ioutil.TempFile(filepath.Dir(file), filepath.Base(file)+".tmp-")
if err != nil {
return err
}
defer os.Remove(f.Name())
defer f.Close()
if _, err := f.Write(data); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
// Rename temp file onto cache file,
// so that the cache file is always a complete file.
if err := os.Rename(f.Name(), file); err != nil {

if err := renameio.WriteFile(file, data); err != nil {
return err
}

Expand All @@ -484,8 +503,18 @@ func rewriteVersionList(dir string) {
base.Fatalf("go: internal error: misuse of rewriteVersionList")
}

// TODO(rsc): We should do some kind of directory locking here,
// to avoid lost updates.
listFile := filepath.Join(dir, "list")

// We use a separate lockfile here instead of locking listFile itself because
// we want to use Rename to write the file atomically. The list may be read by
// a GOPROXY HTTP server, and if we crash midway through a rewrite (or if the
// HTTP server ignores our locking and serves the file midway through a
// rewrite) it's better to serve a stale list than a truncated one.
unlock, err := lockedfile.MutexAt(listFile + ".lock").Lock()
if err != nil {
base.Fatalf("go: can't lock version list lockfile: %v", err)
}
defer unlock()

infos, err := ioutil.ReadDir(dir)
if err != nil {
Expand Down Expand Up @@ -514,12 +543,12 @@ func rewriteVersionList(dir string) {
buf.WriteString(v)
buf.WriteString("\n")
}
listFile := filepath.Join(dir, "list")
old, _ := ioutil.ReadFile(listFile)
if bytes.Equal(buf.Bytes(), old) {
return
}
// TODO: Use rename to install file,
// so that readers never see an incomplete file.
ioutil.WriteFile(listFile, buf.Bytes(), 0666)

if err := renameio.WriteFile(listFile, buf.Bytes()); err != nil {
base.Fatalf("go: failed to write version list: %v", err)
}
}
Loading

0 comments on commit 04e12a5

Please sign in to comment.