| // 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 sourcecache provides a cache of code found in Git repositories. | |
| // It takes directly to the Gerrit instance at go.googlesource.com. | |
| // If RegisterGitMirrorDial is called, it will first try to get code from gitmirror before falling back on Gerrit. | |
| package sourcecache | |
| import ( | |
| "bytes" | |
| "context" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "log" | |
| "net" | |
| "net/http" | |
| "time" | |
| "golang.org/x/build/internal/lru" | |
| "golang.org/x/build/internal/singleflight" | |
| "golang.org/x/build/internal/spanlog" | |
| ) | |
| var processStartTime = time.Now() | |
| var sourceGroup singleflight.Group | |
| var sourceCache = lru.New(40) // repo-rev -> source | |
| // source is the cache entry type for sourceCache. | |
| type source struct { | |
| Tgz []byte // Source tarball bytes. | |
| TooBig bool | |
| } | |
| // GetSourceTgz returns a Reader that provides a tgz of the requested source revision. | |
| // repo is go.googlesource.com repo ("go", "net", and so on). | |
| // rev is git revision. | |
| // | |
| // An error of type TooBigError is returned if the compressed tarball exceeds a size that | |
| // on 2021-11-22 was deemed to be enough to meet expected legitimate future needs for a while. | |
| // See golang.org/issue/46379. | |
| func GetSourceTgz(sl spanlog.Logger, repo, rev string) (tgz io.Reader, err error) { | |
| sp := sl.CreateSpan("get_source", repo+"@"+rev) | |
| defer func() { sp.Done(err) }() | |
| key := fmt.Sprintf("%v-%v", repo, rev) | |
| v, err, _ := sourceGroup.Do(key, func() (any, error) { | |
| if src, ok := sourceCache.Get(key); ok { | |
| return src, nil | |
| } | |
| if gitMirrorClient != nil { | |
| sp := sl.CreateSpan("get_source_from_gitmirror") | |
| src, err := getSourceTgzFromGitMirror(repo, rev) | |
| if err == nil { | |
| sourceCache.Add(key, src) | |
| sp.Done(nil) | |
| return src, nil | |
| } | |
| log.Printf("Error fetching source %s/%s from gitmirror (after %v uptime): %v", | |
| repo, rev, time.Since(processStartTime), err) | |
| sp.Done(errors.New("timeout")) | |
| } | |
| sp := sl.CreateSpan("get_source_from_gerrit", fmt.Sprintf("%v from gerrit", key)) | |
| src, err := getSourceTgzFromGerrit(repo, rev) | |
| sp.Done(err) | |
| if err == nil { | |
| sourceCache.Add(key, src) | |
| } | |
| return src, err | |
| }) | |
| if err != nil { | |
| return nil, err | |
| } | |
| if v.(source).TooBig { | |
| return nil, TooBigError{Repo: repo, Rev: rev, Limit: maxSize(repo)} | |
| } | |
| return bytes.NewReader(v.(source).Tgz), nil | |
| } | |
| // TooBigError is the error returned when the source revision is considered too big. | |
| type TooBigError struct { | |
| Repo string | |
| Rev string | |
| Limit int64 // Max size in bytes. | |
| } | |
| func (e TooBigError) Error() string { | |
| return fmt.Sprintf("rejected because compressed tarball of repository go.googlesource.com/%s at revision %s exceeded a limit of %d MB; see golang.org/issue/46379", | |
| e.Repo, e.Rev, e.Limit/1024/1024) | |
| } | |
| var gitMirrorClient *http.Client | |
| // RegisterGitMirrorDial registers a dial function which will be used to reach gitmirror. | |
| // If used, this function must be called before GetSourceTgz. | |
| func RegisterGitMirrorDial(dial func(context.Context) (net.Conn, error)) { | |
| gitMirrorClient = &http.Client{ | |
| Timeout: 30 * time.Second, | |
| Transport: &http.Transport{ | |
| IdleConnTimeout: 30 * time.Second, | |
| DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { | |
| return dial(ctx) | |
| }, | |
| }, | |
| } | |
| } | |
| var gerritHTTPClient = &http.Client{ | |
| Timeout: 30 * time.Second, | |
| } | |
| func getSourceTgzFromGerrit(repo, rev string) (source, error) { | |
| return getSourceTgzFromURL(gerritHTTPClient, "gerrit", repo, rev, "https://go.googlesource.com/"+repo+"/+archive/"+rev+".tar.gz") | |
| } | |
| func getSourceTgzFromGitMirror(repo, rev string) (src source, err error) { | |
| for i := range 2 { // two tries; different pods maybe? | |
| if i > 0 { | |
| time.Sleep(1 * time.Second) | |
| } | |
| // The "gitmirror" hostname is unused: | |
| src, err = getSourceTgzFromURL(gitMirrorClient, "gitmirror", repo, rev, "http://gitmirror/"+repo+".tar.gz?rev="+rev) | |
| if err == nil { | |
| return src, nil | |
| } | |
| if tr, ok := http.DefaultTransport.(*http.Transport); ok { | |
| tr.CloseIdleConnections() | |
| } | |
| } | |
| return source{}, err | |
| } | |
| // getSourceTgzFromURL fetches a source tarball from url. | |
| // If url serves more than maxSize bytes, it stops short. | |
| func getSourceTgzFromURL(hc *http.Client, service, repo, rev, url string) (source, error) { | |
| res, err := hc.Get(url) | |
| if err != nil { | |
| return source{}, fmt.Errorf("fetching %s/%s from %s: %v", repo, rev, service, err) | |
| } | |
| defer res.Body.Close() | |
| if res.StatusCode/100 != 2 { | |
| slurp, _ := io.ReadAll(io.LimitReader(res.Body, 4<<10)) | |
| return source{}, fmt.Errorf("fetching %s/%s from %s: %v; body: %s", repo, rev, service, res.Status, slurp) | |
| } | |
| // See golang.org/issue/11224 for a discussion on tree filtering. | |
| b, err := io.ReadAll(io.LimitReader(res.Body, maxSize(repo)+1)) | |
| if int64(len(b)) > maxSize(repo) && err == nil { | |
| return source{TooBig: true}, nil | |
| } | |
| if err != nil { | |
| return source{}, fmt.Errorf("reading %s/%s from %s: %v", repo, rev, service, err) | |
| } | |
| return source{Tgz: b}, nil | |
| } | |
| // maxSize controls artificial limits on how big of a compressed source tarball | |
| // this package is willing to accept. It's expected humans may need to manage | |
| // these limits every couple of years for the evolving needs of the Go project, | |
| // and ideally not much more often. | |
| // | |
| // repo is a go.googlesource.com repo ("go", "net", and so on). | |
| func maxSize(repo string) int64 { | |
| switch repo { | |
| default: | |
| // As of 2021-11-22, a compressed tarball of Go source is 23 MB, | |
| // x/net is 1.2 MB, | |
| // x/build is 1.1 MB, | |
| // x/tools is 2.9 MB. | |
| return 100 << 20 | |
| case "website": | |
| // In 2021, all content in x/blog (52 MB) and x/talks (74 MB) moved | |
| // to x/website. This makes x/website an outlier, with a compressed | |
| // tarball size of 135 MB. Give it some room to grow from there. | |
| return 200 << 20 | |
| } | |
| } |