golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/coordinator/internal/legacydash/build.go (about)

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build linux || darwin
     6  
     7  package legacydash
     8  
     9  import (
    10  	"bytes"
    11  	"compress/gzip"
    12  	"context"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"math/rand"
    17  	pathpkg "path"
    18  	"strings"
    19  
    20  	"cloud.google.com/go/datastore"
    21  	"golang.org/x/build/dashboard"
    22  	"golang.org/x/build/internal/loghash"
    23  )
    24  
    25  const (
    26  	maxDatastoreStringLen = 500
    27  )
    28  
    29  func dsKey(kind, name string, parent *datastore.Key) *datastore.Key {
    30  	dk := datastore.NameKey(kind, name, parent)
    31  	dk.Namespace = "Git"
    32  	return dk
    33  }
    34  
    35  // A Package describes a package that is listed on the dashboard.
    36  type Package struct {
    37  	Name string // "Go", "arch", "net", ...
    38  	Path string // empty for the main Go tree, else "golang.org/x/foo"
    39  }
    40  
    41  func (p *Package) String() string {
    42  	return fmt.Sprintf("%s: %q", p.Path, p.Name)
    43  }
    44  
    45  func (p *Package) Key() *datastore.Key {
    46  	key := p.Path
    47  	if key == "" {
    48  		key = "go"
    49  	}
    50  	return dsKey("Package", key, nil)
    51  }
    52  
    53  // filterDatastoreError returns err, unless it's just about datastore
    54  // not being able to load an entity with old legacy struct fields into
    55  // the Commit type that has since removed those fields.
    56  func filterDatastoreError(err error) error {
    57  	return filterAppEngineError(err, func(err error) bool {
    58  		if em, ok := err.(*datastore.ErrFieldMismatch); ok {
    59  			switch em.FieldName {
    60  			case "NeedsBenchmarking", "TryPatch", "FailNotificationSent":
    61  				// Removed in CLs 208397 and 208324.
    62  				return true
    63  			case "PackagePath", "ParentHash", "Num", "User", "Desc", "Time", "Branch", "NextNum", "Kind":
    64  				// Removed in move to maintner in CL 208697.
    65  				return true
    66  			}
    67  		}
    68  		return false
    69  	})
    70  }
    71  
    72  // filterNoSuchEntity returns err, unless it's just about datastore
    73  // not being able to load an entity because it doesn't exist.
    74  func filterNoSuchEntity(err error) error {
    75  	return filterAppEngineError(err, func(err error) bool {
    76  		return err == datastore.ErrNoSuchEntity
    77  	})
    78  }
    79  
    80  // filterAppEngineError returns err, unless ignore(err) is true,
    81  // in which case it returns nil. If err is an datastore.MultiError,
    82  // it returns either nil (if all errors are ignored) or a deep copy
    83  // with the non-ignored errors.
    84  func filterAppEngineError(err error, ignore func(error) bool) error {
    85  	if err == nil || ignore(err) {
    86  		return nil
    87  	}
    88  	if me, ok := err.(datastore.MultiError); ok {
    89  		me2 := make(datastore.MultiError, 0, len(me))
    90  		for _, err := range me {
    91  			if e2 := filterAppEngineError(err, ignore); e2 != nil {
    92  				me2 = append(me2, e2)
    93  			}
    94  		}
    95  		if len(me2) == 0 {
    96  			return nil
    97  		}
    98  		return me2
    99  	}
   100  	return err
   101  }
   102  
   103  // getOrMakePackageInTx fetches a Package by path from the datastore,
   104  // creating it if necessary.
   105  func getOrMakePackageInTx(ctx context.Context, tx *datastore.Transaction, path string) (*Package, error) {
   106  	p := &Package{Path: path}
   107  	if path != "" {
   108  		p.Name = pathpkg.Base(path)
   109  	} else {
   110  		p.Name = "Go"
   111  	}
   112  	err := tx.Get(p.Key(), p)
   113  	err = filterDatastoreError(err)
   114  	if err == datastore.ErrNoSuchEntity {
   115  		if _, err := tx.Put(p.Key(), p); err != nil {
   116  			return nil, err
   117  		}
   118  		return p, nil
   119  	}
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	return p, nil
   124  }
   125  
   126  type builderAndGoHash struct {
   127  	builder, goHash string
   128  }
   129  
   130  // A Commit describes an individual commit in a package.
   131  //
   132  // Each Commit entity is a descendant of its associated Package entity.
   133  // In other words, all Commits with the same PackagePath belong to the same
   134  // datastore entity group.
   135  type Commit struct {
   136  	PackagePath string // (empty for main repo commits)
   137  	Hash        string
   138  
   139  	// ResultData is the Data string of each build Result for this Commit.
   140  	// For non-Go commits, only the Results for the current Go tip, weekly,
   141  	// and release Tags are stored here. This is purely de-normalized data.
   142  	// The complete data set is stored in Result entities.
   143  	//
   144  	// Each string is formatted as builder|OK|LogHash|GoHash.
   145  	ResultData []string `datastore:",noindex"`
   146  }
   147  
   148  func (com *Commit) Key() *datastore.Key {
   149  	if com.Hash == "" {
   150  		panic("tried Key on Commit with empty Hash")
   151  	}
   152  	p := Package{Path: com.PackagePath}
   153  	key := com.PackagePath + "|" + com.Hash
   154  	return dsKey("Commit", key, p.Key())
   155  }
   156  
   157  // Valid reports whether the commit is valid.
   158  func (c *Commit) Valid() bool {
   159  	// Valid really just means the hash is populated.
   160  	return validHash(c.Hash)
   161  }
   162  
   163  // each result line is approx 105 bytes. This constant is a tradeoff between
   164  // build history and the AppEngine datastore limit of 1mb.
   165  const maxResults = 1000
   166  
   167  // AddResult adds the denormalized Result data to the Commit's
   168  // ResultData field.
   169  func (com *Commit) AddResult(tx *datastore.Transaction, r *Result) error {
   170  	err := tx.Get(com.Key(), com)
   171  	if err == datastore.ErrNoSuchEntity {
   172  		// If it doesn't exist, we create it below.
   173  	} else {
   174  		err = filterDatastoreError(err)
   175  		if err != nil {
   176  			return fmt.Errorf("Commit.AddResult, getting Commit: %v", err)
   177  		}
   178  	}
   179  
   180  	var resultExists bool
   181  	for i, s := range com.ResultData {
   182  		// if there already exists result data for this builder at com, overwrite it.
   183  		if strings.HasPrefix(s, r.Builder+"|") && strings.HasSuffix(s, "|"+r.GoHash) {
   184  			resultExists = true
   185  			com.ResultData[i] = r.Data()
   186  		}
   187  	}
   188  	if !resultExists {
   189  		// otherwise, add the new result data for this builder.
   190  		com.ResultData = trim(append(com.ResultData, r.Data()), maxResults)
   191  	}
   192  	if !com.Valid() {
   193  		return errors.New("putting Commit: commit is not valid")
   194  	}
   195  	if _, err := tx.Put(com.Key(), com); err != nil {
   196  		return fmt.Errorf("putting Commit: %v", err)
   197  	}
   198  	return nil
   199  }
   200  
   201  // RemoveResult removes the denormalized Result data from the ResultData field
   202  // for the given builder and go hash.
   203  // It must be called from within the datastore transaction that gets and puts
   204  // the Commit. Note this is slightly different to AddResult, above.
   205  func (com *Commit) RemoveResult(r *Result) {
   206  	var rd []string
   207  	for _, s := range com.ResultData {
   208  		if strings.HasPrefix(s, r.Builder+"|") && strings.HasSuffix(s, "|"+r.GoHash) {
   209  			continue
   210  		}
   211  		rd = append(rd, s)
   212  	}
   213  	com.ResultData = rd
   214  }
   215  
   216  func trim(s []string, n int) []string {
   217  	l := min(len(s), n)
   218  	return s[len(s)-l:]
   219  }
   220  
   221  func min(a, b int) int {
   222  	if a < b {
   223  		return a
   224  	}
   225  	return b
   226  }
   227  
   228  // Result returns the build Result for this Commit for the given builder/goHash.
   229  //
   230  // For the main Go repo, goHash is the empty string.
   231  func (c *Commit) Result(builder, goHash string) *Result {
   232  	r := result(c.ResultData, c.Hash, c.PackagePath, builder, goHash)
   233  	if r == nil {
   234  		return nil
   235  	}
   236  	return &r.Result
   237  }
   238  
   239  // Result returns the build Result for this commit for the given builder/goHash.
   240  //
   241  // For the main Go repo, goHash is the empty string.
   242  func (c *CommitInfo) Result(builder, goHash string) *DisplayResult {
   243  	if r := result(c.ResultData, c.Hash, c.PackagePath, builder, goHash); r != nil {
   244  		return r
   245  	}
   246  	if u, ok := c.BuildingURLs[builderAndGoHash{builder, goHash}]; ok {
   247  		return &DisplayResult{Result: Result{
   248  			Builder:     builder,
   249  			BuildingURL: u,
   250  			Hash:        c.Hash,
   251  			GoHash:      goHash,
   252  		}}
   253  	}
   254  	if fakeResults {
   255  		// Create a fake random result.
   256  		switch rand.Intn(3) {
   257  		default:
   258  			return nil
   259  		case 1:
   260  			return &DisplayResult{Result: Result{
   261  				Builder: builder,
   262  				Hash:    c.Hash,
   263  				GoHash:  goHash,
   264  				OK:      true,
   265  			}}
   266  		case 2:
   267  			return &DisplayResult{Result: Result{
   268  				Builder: builder,
   269  				Hash:    c.Hash,
   270  				GoHash:  goHash,
   271  				LogHash: "fakefailureurl",
   272  			}}
   273  		}
   274  	}
   275  	return nil
   276  }
   277  
   278  type DisplayResult struct {
   279  	Result
   280  	Noise bool
   281  }
   282  
   283  func (r *DisplayResult) LogURL() string {
   284  	if strings.HasPrefix(r.LogHash, "https://") {
   285  		return r.LogHash
   286  	} else {
   287  		return "/log/" + r.LogHash
   288  	}
   289  }
   290  
   291  func result(resultData []string, hash, packagePath, builder, goHash string) *DisplayResult {
   292  	for _, r := range resultData {
   293  		if !strings.HasPrefix(r, builder) {
   294  			// Avoid strings.SplitN alloc in the common case.
   295  			continue
   296  		}
   297  		p := strings.SplitN(r, "|", 4)
   298  		if len(p) != 4 || p[0] != builder || p[3] != goHash {
   299  			continue
   300  		}
   301  		return partsToResult(hash, packagePath, p)
   302  	}
   303  	return nil
   304  }
   305  
   306  // isUntested reports whether a cell in the build.golang.org grid is
   307  // an untested configuration.
   308  //
   309  // repo is "go", "net", etc.
   310  // branch is the branch of repo "master" or "release-branch.go1.12"
   311  // goBranch applies only if repo != "go" and is of form "master" or "release-branch.go1.N"
   312  //
   313  // As a special case, "tip" is an alias for "master", since this app
   314  // still uses a bunch of hg terms from when we used hg.
   315  func isUntested(builder, repo, branch, goBranch string) bool {
   316  	if strings.HasSuffix(builder, "-🐇") {
   317  		// LUCI builders are never considered untested.
   318  		//
   319  		// Note: It would be possible to improve this by reporting
   320  		// whether a given LUCI builder exists for a given x/ repo.
   321  		// That needs more bookkeeping and code, so left for later.
   322  		return false
   323  	}
   324  	if branch == "tip" {
   325  		branch = "master"
   326  	}
   327  	if goBranch == "tip" {
   328  		goBranch = "master"
   329  	}
   330  	bc, ok := dashboard.Builders[builder]
   331  	if !ok {
   332  		// Unknown builder, so not tested.
   333  		return true
   334  	}
   335  	return !bc.BuildsRepoPostSubmit(repo, branch, goBranch)
   336  }
   337  
   338  // knownIssue returns a known issue for the named builder,
   339  // or zero if there isn't a known issue.
   340  func knownIssue(builder string) int {
   341  	bc, ok := dashboard.Builders[builder]
   342  	if !ok {
   343  		// Unknown builder.
   344  		return 0
   345  	}
   346  	if len(bc.KnownIssues) > 0 {
   347  		return bc.KnownIssues[0]
   348  	}
   349  	return 0
   350  }
   351  
   352  // Results returns the build results for this Commit.
   353  func (c *CommitInfo) Results() (results []*DisplayResult) {
   354  	for _, r := range c.ResultData {
   355  		p := strings.SplitN(r, "|", 4)
   356  		if len(p) != 4 {
   357  			continue
   358  		}
   359  		results = append(results, partsToResult(c.Hash, c.PackagePath, p))
   360  	}
   361  	return
   362  }
   363  
   364  // ResultGoHashes, for non-go repos, returns the list of Go hashes that
   365  // this repo has been (or should be) built at.
   366  //
   367  // For the main Go repo it always returns a slice with 1 element: the
   368  // empty string.
   369  func (c *CommitInfo) ResultGoHashes() []string {
   370  	// For the main repo, just return the empty string
   371  	// (there's no corresponding main repo hash for a main repo Commit).
   372  	// This function is only really useful for sub-repos.
   373  	if c.PackagePath == "" {
   374  		return []string{""}
   375  	}
   376  	var hashes []string
   377  	for _, r := range c.ResultData {
   378  		p := strings.SplitN(r, "|", 4)
   379  		if len(p) != 4 {
   380  			continue
   381  		}
   382  		// Append only new results (use linear scan to preserve order).
   383  		if !contains(hashes, p[3]) {
   384  			hashes = append(hashes, p[3])
   385  		}
   386  	}
   387  	// Return results in reverse order (newest first).
   388  	reverse(hashes)
   389  	return hashes
   390  }
   391  
   392  func contains(t []string, s string) bool {
   393  	for _, s2 := range t {
   394  		if s2 == s {
   395  			return true
   396  		}
   397  	}
   398  	return false
   399  }
   400  
   401  func reverse(s []string) {
   402  	for i := 0; i < len(s)/2; i++ {
   403  		j := len(s) - i - 1
   404  		s[i], s[j] = s[j], s[i]
   405  	}
   406  }
   407  
   408  // partsToResult creates a DisplayResult from ResultData substrings.
   409  func partsToResult(hash, packagePath string, p []string) *DisplayResult {
   410  	return &DisplayResult{
   411  		Result: Result{
   412  			Builder:     p[0],
   413  			Hash:        hash,
   414  			PackagePath: packagePath,
   415  			GoHash:      p[3],
   416  			OK:          p[1] == "true",
   417  			LogHash:     p[2],
   418  		},
   419  		Noise: p[1] == "infra_failure",
   420  	}
   421  }
   422  
   423  // A Result describes a build result for a Commit on an OS/architecture.
   424  //
   425  // Each Result entity is a descendant of its associated Package entity.
   426  type Result struct {
   427  	Builder     string // "os-arch[-note]"
   428  	PackagePath string // (empty for Go commits, else "golang.org/x/foo")
   429  	Hash        string
   430  
   431  	// The Go Commit this was built against (when PackagePath != ""; empty for Go commits).
   432  	GoHash string
   433  
   434  	BuildingURL string `datastore:"-"` // non-empty if currently building
   435  	OK          bool
   436  	Log         string `datastore:"-"`        // for JSON unmarshaling only
   437  	LogHash     string `datastore:",noindex"` // Key to the Log record.
   438  
   439  	RunTime int64 // time to build+test in nanoseconds
   440  }
   441  
   442  func (r *Result) Key() *datastore.Key {
   443  	p := Package{Path: r.PackagePath}
   444  	key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash
   445  	return dsKey("Result", key, p.Key())
   446  }
   447  
   448  func (r *Result) Valid() error {
   449  	if !validHash(r.Hash) {
   450  		return errors.New("invalid Hash")
   451  	}
   452  	if r.PackagePath != "" && !validHash(r.GoHash) {
   453  		return errors.New("invalid GoHash")
   454  	}
   455  	return nil
   456  }
   457  
   458  // Data returns the Result in string format
   459  // to be stored in Commit's ResultData field.
   460  func (r *Result) Data() string {
   461  	return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
   462  }
   463  
   464  // A Log is a gzip-compressed log file stored under the SHA1 hash of the
   465  // uncompressed log text.
   466  type Log struct {
   467  	CompressedLog []byte `datastore:",noindex"`
   468  }
   469  
   470  func (l *Log) Text() ([]byte, error) {
   471  	d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog))
   472  	if err != nil {
   473  		return nil, fmt.Errorf("reading log data: %v", err)
   474  	}
   475  	b, err := io.ReadAll(d)
   476  	if err != nil {
   477  		return nil, fmt.Errorf("reading log data: %v", err)
   478  	}
   479  	return b, nil
   480  }
   481  
   482  func (h handler) putLog(c context.Context, text string) (hash string, err error) {
   483  	b := new(bytes.Buffer)
   484  	z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
   485  	io.WriteString(z, text)
   486  	z.Close()
   487  	hash = loghash.New(text)
   488  	key := dsKey("Log", hash, nil)
   489  	_, err = h.datastoreCl.Put(c, key, &Log{b.Bytes()})
   490  	return
   491  }