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 }