Skip to content

Commit 00c6f96

Browse files
committed
cmd/go/internal/modfetch: cache info and gomod on disk
This on-disk caching was previously done inside package vgo, but that means that other code calling modfetch directly does not benefit from the cache entries and does unnecessary network operations. Fix that. This CL also renames the local cache directory root from $GOPATH/src/v to $GOPATH/src/mod. The "v" stood for versioned, but "mod" seems clearer (it's the downloaded modules). This CL also fixes a deadlock in the caching code: one Repo's GoMod may need to call another repo's Stat and vice versa, so it can't be that a Repo must only be used from one goroutine at a time, or else we'll end up with a deadlock. Redefine that any Repo must be allowed to be used from multiple goroutines simultaneously and update the code accordingly. This eliminates the potential deadlock. This CL also makes the gitrepo implementation work a bit harder to use its local information before doing any network operations. In particular, locally-cached tags or commits are now resolved without consulting the remote repo. Fixes golang/go#25919. Change-Id: I3443ffc97a1e3e465c8780fb81f263be3b6d77ae Reviewed-on: https://go-review.googlesource.com/119475 Run-TryBot: Russ Cox <[email protected]> Reviewed-by: Bryan C. Mills <[email protected]>
1 parent 22e2390 commit 00c6f96

File tree

11 files changed

+404
-129
lines changed

11 files changed

+404
-129
lines changed

vendor/cmd/go/internal/modcmd/verify.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ func runVerify() {
2929

3030
func verifyMod(mod module.Version) bool {
3131
ok := true
32-
zip := filepath.Join(vgo.SrcV, "cache", mod.Path, "/@v/", mod.Version+".zip")
32+
zip := filepath.Join(vgo.SrcMod, "cache", mod.Path, "/@v/", mod.Version+".zip")
3333
_, zipErr := os.Stat(zip)
34-
dir := filepath.Join(vgo.SrcV, mod.Path+"@"+mod.Version)
34+
dir := filepath.Join(vgo.SrcMod, mod.Path+"@"+mod.Version)
3535
_, dirErr := os.Stat(dir)
3636
data, err := ioutil.ReadFile(zip + "hash")
3737
if err != nil {

vendor/cmd/go/internal/modfetch/cache.go

Lines changed: 252 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,24 @@
55
package modfetch
66

77
import (
8-
"sync"
8+
"bytes"
9+
"encoding/json"
10+
"fmt"
11+
"io/ioutil"
12+
"os"
13+
"path/filepath"
14+
"strings"
915

16+
"cmd/go/internal/modconv"
17+
"cmd/go/internal/modfetch/codehost"
1018
"cmd/go/internal/par"
19+
"cmd/go/internal/semver"
1120
)
1221

22+
var QuietLookup bool // do not print about lookups
23+
24+
var CacheRoot string // $GOPATH/src/mod/cache
25+
1326
// A cachingRepo is a cache around an underlying Repo,
1427
// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
1528
// It is also safe for simultaneous use by multiple goroutines
@@ -18,9 +31,7 @@ import (
1831
type cachingRepo struct {
1932
path string
2033
cache par.Cache // cache for all operations
21-
22-
mu sync.Mutex // protects r's methods
23-
r Repo
34+
r Repo
2435
}
2536

2637
func newCachingRepo(r Repo) *cachingRepo {
@@ -40,8 +51,6 @@ func (r *cachingRepo) Versions(prefix string) ([]string, error) {
4051
err error
4152
}
4253
c := r.cache.Do("versions:"+prefix, func() interface{} {
43-
r.mu.Lock()
44-
defer r.mu.Unlock()
4554
list, err := r.r.Versions(prefix)
4655
return cached{list, err}
4756
}).(cached)
@@ -52,17 +61,36 @@ func (r *cachingRepo) Versions(prefix string) ([]string, error) {
5261
return append([]string(nil), c.list...), nil
5362
}
5463

64+
type cachedInfo struct {
65+
info *RevInfo
66+
err error
67+
}
68+
5569
func (r *cachingRepo) Stat(rev string) (*RevInfo, error) {
56-
type cached struct {
57-
info *RevInfo
58-
err error
59-
}
6070
c := r.cache.Do("stat:"+rev, func() interface{} {
61-
r.mu.Lock()
62-
defer r.mu.Unlock()
63-
info, err := r.r.Stat(rev)
64-
return cached{info, err}
65-
}).(cached)
71+
file, info, err := readDiskStat(r.path, rev)
72+
if err == nil {
73+
return cachedInfo{info, nil}
74+
}
75+
76+
if !QuietLookup {
77+
fmt.Fprintf(os.Stderr, "vgo: finding %s %s\n", r.path, rev)
78+
}
79+
info, err = r.r.Stat(rev)
80+
if err == nil {
81+
if err := writeDiskStat(file, info); err != nil {
82+
fmt.Fprintf(os.Stderr, "go: writing stat cache: %v\n", err)
83+
}
84+
// If we resolved, say, 1234abcde to v0.0.0-20180604122334-1234abcdef78,
85+
// then save the information under the proper version, for future use.
86+
if info.Version != rev {
87+
r.cache.Do("stat:"+info.Version, func() interface{} {
88+
return cachedInfo{info, err}
89+
})
90+
}
91+
}
92+
return cachedInfo{info, err}
93+
}).(cachedInfo)
6694

6795
if c.err != nil {
6896
return nil, c.err
@@ -72,16 +100,28 @@ func (r *cachingRepo) Stat(rev string) (*RevInfo, error) {
72100
}
73101

74102
func (r *cachingRepo) Latest() (*RevInfo, error) {
75-
type cached struct {
103+
type cachedInfo struct {
76104
info *RevInfo
77105
err error
78106
}
79107
c := r.cache.Do("latest:", func() interface{} {
80-
r.mu.Lock()
81-
defer r.mu.Unlock()
108+
if !QuietLookup {
109+
fmt.Fprintf(os.Stderr, "vgo: finding %s latest\n", r.path)
110+
}
82111
info, err := r.r.Latest()
83-
return cached{info, err}
84-
}).(cached)
112+
113+
// Save info for likely future Stat call.
114+
if err == nil {
115+
r.cache.Do("stat:"+info.Version, func() interface{} {
116+
return cachedInfo{info, err}
117+
})
118+
if file, _, err := readDiskStat(r.path, info.Version); err != nil {
119+
writeDiskStat(file, info)
120+
}
121+
}
122+
123+
return cachedInfo{info, err}
124+
}).(cachedInfo)
85125

86126
if c.err != nil {
87127
return nil, c.err
@@ -96,9 +136,17 @@ func (r *cachingRepo) GoMod(rev string) ([]byte, error) {
96136
err error
97137
}
98138
c := r.cache.Do("gomod:"+rev, func() interface{} {
99-
r.mu.Lock()
100-
defer r.mu.Unlock()
101-
text, err := r.r.GoMod(rev)
139+
file, text, err := readDiskGoMod(r.path, rev)
140+
if err == nil {
141+
return cached{text, nil}
142+
}
143+
144+
text, err = r.r.GoMod(rev)
145+
if err == nil {
146+
if err := writeDiskGoMod(file, text); err != nil {
147+
fmt.Fprintf(os.Stderr, "go: writing go.mod cache: %v\n", err)
148+
}
149+
}
102150
return cached{text, err}
103151
}).(cached)
104152

@@ -109,7 +157,186 @@ func (r *cachingRepo) GoMod(rev string) ([]byte, error) {
109157
}
110158

111159
func (r *cachingRepo) Zip(version, tmpdir string) (string, error) {
112-
r.mu.Lock()
113-
defer r.mu.Unlock()
114160
return r.r.Zip(version, tmpdir)
115161
}
162+
163+
// Stat is like Lookup(path).Stat(rev) but avoids the
164+
// repository path resolution in Lookup if the result is
165+
// already cached on local disk.
166+
func Stat(path, rev string) (*RevInfo, error) {
167+
_, info, err := readDiskStat(path, rev)
168+
if err == nil {
169+
return info, nil
170+
}
171+
repo, err := Lookup(path)
172+
if err != nil {
173+
return nil, err
174+
}
175+
return repo.Stat(rev)
176+
}
177+
178+
// GoMod is like Lookup(path).GoMod(rev) but avoids the
179+
// repository path resolution in Lookup if the result is
180+
// already cached on local disk.
181+
func GoMod(path, rev string) ([]byte, error) {
182+
// Convert commit hash to pseudo-version
183+
// to increase cache hit rate.
184+
if !semver.IsValid(rev) {
185+
info, err := Stat(path, rev)
186+
if err != nil {
187+
return nil, err
188+
}
189+
rev = info.Version
190+
}
191+
_, data, err := readDiskGoMod(path, rev)
192+
if err == nil {
193+
return data, nil
194+
}
195+
repo, err := Lookup(path)
196+
if err != nil {
197+
return nil, err
198+
}
199+
return repo.GoMod(rev)
200+
}
201+
202+
var errNotCached = fmt.Errorf("not in cache")
203+
204+
// readDiskStat reads a cached stat result from disk,
205+
// returning the name of the cache file and the result.
206+
// If the read fails, the caller can use
207+
// writeDiskStat(file, info) to write a new cache entry.
208+
func readDiskStat(path, rev string) (file string, info *RevInfo, err error) {
209+
file, data, err := readDiskCache(path, rev, "info")
210+
if err != nil {
211+
if file, info, err := readDiskStatByHash(path, rev); err == nil {
212+
return file, info, nil
213+
}
214+
return file, nil, err
215+
}
216+
info = new(RevInfo)
217+
if err := json.Unmarshal(data, info); err != nil {
218+
return file, nil, errNotCached
219+
}
220+
return file, info, nil
221+
}
222+
223+
// readDiskStatByHash is a fallback for readDiskStat for the case
224+
// where rev is a commit hash instead of a proper semantic version.
225+
// In that case, we look for a cached pseudo-version that matches
226+
// the commit hash. If we find one, we use it.
227+
// This matters most for converting legacy package management
228+
// configs, when we are often looking up commits by full hash.
229+
// Without this check we'd be doing network I/O to the remote repo
230+
// just to find out about a commit we already know about
231+
// (and have cached under its pseudo-version).
232+
func readDiskStatByHash(path, rev string) (file string, info *RevInfo, err error) {
233+
if !codehost.AllHex(rev) || len(rev) < 12 {
234+
return "", nil, errNotCached
235+
}
236+
rev = rev[:12]
237+
dir, err := os.Open(filepath.Join(CacheRoot, path, "@v"))
238+
if err != nil {
239+
return "", nil, errNotCached
240+
}
241+
names, err := dir.Readdirnames(-1)
242+
dir.Close()
243+
if err != nil {
244+
return "", nil, errNotCached
245+
}
246+
suffix := "-" + rev + ".info"
247+
for _, name := range names {
248+
if strings.HasSuffix(name, suffix) && isPseudoVersion(strings.TrimSuffix(name, ".info")) {
249+
return readDiskStat(path, strings.TrimSuffix(name, ".info"))
250+
}
251+
}
252+
return "", nil, errNotCached
253+
}
254+
255+
var vgoVersion = []byte(modconv.Prefix)
256+
257+
// readDiskGoMod reads a cached stat result from disk,
258+
// returning the name of the cache file and the result.
259+
// If the read fails, the caller can use
260+
// writeDiskGoMod(file, data) to write a new cache entry.
261+
func readDiskGoMod(path, rev string) (file string, data []byte, err error) {
262+
file, data, err = readDiskCache(path, rev, "mod")
263+
264+
// If go.mod has a //vgo comment at the start,
265+
// it was auto-converted from a legacy lock file.
266+
// The auto-conversion details may have bugs and
267+
// may be fixed in newer versions of vgo.
268+
// We ignore cached go.mod files if they do not match
269+
// our own vgoVersion.
270+
// This use of "vgo" appears in disk files and must be preserved
271+
// even once we excise most of the mentions of vgo from the code.
272+
if err == nil && bytes.HasPrefix(data, vgoVersion[:len("//vgo")]) && !bytes.HasPrefix(data, vgoVersion) {
273+
err = errNotCached
274+
data = nil
275+
}
276+
277+
return file, data, err
278+
}
279+
280+
// readDiskCache is the generic "read from a cache file" implementation.
281+
// It takes the revision and an identifying suffix for the kind of data being cached.
282+
// It returns the name of the cache file and the content of the file.
283+
// If the read fails, the caller can use
284+
// writeDiskCache(file, data) to write a new cache entry.
285+
func readDiskCache(path, rev, suffix string) (file string, data []byte, err error) {
286+
if !semver.IsValid(rev) || CacheRoot == "" {
287+
return "", nil, errNotCached
288+
}
289+
file = filepath.Join(CacheRoot, path, "@v", rev+"."+suffix)
290+
data, err = ioutil.ReadFile(file)
291+
if err != nil {
292+
return file, nil, errNotCached
293+
}
294+
return file, data, nil
295+
}
296+
297+
// writeDiskStat writes a stat result cache entry.
298+
// The file name must have been returned by a previous call to readDiskStat.
299+
func writeDiskStat(file string, info *RevInfo) error {
300+
if file == "" {
301+
return nil
302+
}
303+
js, err := json.Marshal(info)
304+
if err != nil {
305+
return err
306+
}
307+
return writeDiskCache(file, js)
308+
}
309+
310+
// writeDiskGoMod writes a go.mod cache entry.
311+
// The file name must have been returned by a previous call to readDiskGoMod.
312+
func writeDiskGoMod(file string, text []byte) error {
313+
return writeDiskCache(file, text)
314+
}
315+
316+
// writeDiskCache is the generic "write to a cache file" implementation.
317+
// The file must have been returned by a previous call to readDiskCache.
318+
func writeDiskCache(file string, data []byte) error {
319+
if file == "" {
320+
return nil
321+
}
322+
// Make sure directory for file exists.
323+
if err := os.MkdirAll(filepath.Dir(file), 0777); err != nil {
324+
return err
325+
}
326+
// Write data to temp file next to target file.
327+
f, err := ioutil.TempFile(filepath.Dir(file), filepath.Base(file)+".tmp*")
328+
if err != nil {
329+
return err
330+
}
331+
defer os.Remove(f.Name())
332+
defer f.Close()
333+
if _, err := f.Write(data); err != nil {
334+
return err
335+
}
336+
if err := f.Close(); err != nil {
337+
return err
338+
}
339+
// Rename temp file onto cache file,
340+
// so that the cache file is always a complete file.
341+
return os.Rename(f.Name(), file)
342+
}

0 commit comments

Comments
 (0)