5
5
package modfetch
6
6
7
7
import (
8
- "sync"
8
+ "bytes"
9
+ "encoding/json"
10
+ "fmt"
11
+ "io/ioutil"
12
+ "os"
13
+ "path/filepath"
14
+ "strings"
9
15
16
+ "cmd/go/internal/modconv"
17
+ "cmd/go/internal/modfetch/codehost"
10
18
"cmd/go/internal/par"
19
+ "cmd/go/internal/semver"
11
20
)
12
21
22
+ var QuietLookup bool // do not print about lookups
23
+
24
+ var CacheRoot string // $GOPATH/src/mod/cache
25
+
13
26
// A cachingRepo is a cache around an underlying Repo,
14
27
// avoiding redundant calls to ModulePath, Versions, Stat, Latest, and GoMod (but not Zip).
15
28
// It is also safe for simultaneous use by multiple goroutines
@@ -18,9 +31,7 @@ import (
18
31
type cachingRepo struct {
19
32
path string
20
33
cache par.Cache // cache for all operations
21
-
22
- mu sync.Mutex // protects r's methods
23
- r Repo
34
+ r Repo
24
35
}
25
36
26
37
func newCachingRepo (r Repo ) * cachingRepo {
@@ -40,8 +51,6 @@ func (r *cachingRepo) Versions(prefix string) ([]string, error) {
40
51
err error
41
52
}
42
53
c := r .cache .Do ("versions:" + prefix , func () interface {} {
43
- r .mu .Lock ()
44
- defer r .mu .Unlock ()
45
54
list , err := r .r .Versions (prefix )
46
55
return cached {list , err }
47
56
}).(cached )
@@ -52,17 +61,36 @@ func (r *cachingRepo) Versions(prefix string) ([]string, error) {
52
61
return append ([]string (nil ), c .list ... ), nil
53
62
}
54
63
64
+ type cachedInfo struct {
65
+ info * RevInfo
66
+ err error
67
+ }
68
+
55
69
func (r * cachingRepo ) Stat (rev string ) (* RevInfo , error ) {
56
- type cached struct {
57
- info * RevInfo
58
- err error
59
- }
60
70
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 )
66
94
67
95
if c .err != nil {
68
96
return nil , c .err
@@ -72,16 +100,28 @@ func (r *cachingRepo) Stat(rev string) (*RevInfo, error) {
72
100
}
73
101
74
102
func (r * cachingRepo ) Latest () (* RevInfo , error ) {
75
- type cached struct {
103
+ type cachedInfo struct {
76
104
info * RevInfo
77
105
err error
78
106
}
79
107
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
+ }
82
111
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 )
85
125
86
126
if c .err != nil {
87
127
return nil , c .err
@@ -96,9 +136,17 @@ func (r *cachingRepo) GoMod(rev string) ([]byte, error) {
96
136
err error
97
137
}
98
138
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
+ }
102
150
return cached {text , err }
103
151
}).(cached )
104
152
@@ -109,7 +157,186 @@ func (r *cachingRepo) GoMod(rev string) ([]byte, error) {
109
157
}
110
158
111
159
func (r * cachingRepo ) Zip (version , tmpdir string ) (string , error ) {
112
- r .mu .Lock ()
113
- defer r .mu .Unlock ()
114
160
return r .r .Zip (version , tmpdir )
115
161
}
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