blob: d2ced216219d2426880c18ec77635ee4d5c93495 [file] [log] [blame]
// Copyright 2011 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 main
import (
"bytes"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"math/rand"
pathpkg "path"
"strings"
"cloud.google.com/go/datastore"
"golang.org/x/build/dashboard"
"golang.org/x/build/internal/loghash"
)
const (
maxDatastoreStringLen = 500
)
func dsKey(kind, name string, parent *datastore.Key) *datastore.Key {
dk := datastore.NameKey(kind, name, parent)
dk.Namespace = "Git"
return dk
}
// A Package describes a package that is listed on the dashboard.
type Package struct {
Name string // "Go", "arch", "net", ...
Path string // empty for the main Go tree, else "golang.org/x/foo"
}
func (p *Package) String() string {
return fmt.Sprintf("%s: %q", p.Path, p.Name)
}
func (p *Package) Key() *datastore.Key {
key := p.Path
if key == "" {
key = "go"
}
return dsKey("Package", key, nil)
}
// filterDatastoreError returns err, unless it's just about datastore
// not being able to load an entity with old legacy struct fields into
// the Commit type that has since removed those fields.
func filterDatastoreError(err error) error {
return filterAppEngineError(err, func(err error) bool {
if em, ok := err.(*datastore.ErrFieldMismatch); ok {
switch em.FieldName {
case "NeedsBenchmarking", "TryPatch", "FailNotificationSent":
// Removed in CLs 208397 and 208324.
return true
case "PackagePath", "ParentHash", "Num", "User", "Desc", "Time", "Branch", "NextNum", "Kind":
// Removed in move to maintner in CL 208697.
return true
}
}
return false
})
}
// filterNoSuchEntity returns err, unless it's just about datastore
// not being able to load an entity because it doesn't exist.
func filterNoSuchEntity(err error) error {
return filterAppEngineError(err, func(err error) bool {
return err == datastore.ErrNoSuchEntity
})
}
// filterAppEngineError returns err, unless ignore(err) is true,
// in which case it returns nil. If err is an datastore.MultiError,
// it returns either nil (if all errors are ignored) or a deep copy
// with the non-ignored errors.
func filterAppEngineError(err error, ignore func(error) bool) error {
if err == nil || ignore(err) {
return nil
}
if me, ok := err.(datastore.MultiError); ok {
me2 := make(datastore.MultiError, 0, len(me))
for _, err := range me {
if e2 := filterAppEngineError(err, ignore); e2 != nil {
me2 = append(me2, e2)
}
}
if len(me2) == 0 {
return nil
}
return me2
}
return err
}
// getOrMakePackageInTx fetches a Package by path from the datastore,
// creating it if necessary.
func getOrMakePackageInTx(ctx context.Context, tx *datastore.Transaction, path string) (*Package, error) {
p := &Package{Path: path}
if path != "" {
p.Name = pathpkg.Base(path)
} else {
p.Name = "Go"
}
err := tx.Get(p.Key(), p)
err = filterDatastoreError(err)
if err == datastore.ErrNoSuchEntity {
if _, err := tx.Put(p.Key(), p); err != nil {
return nil, err
}
return p, nil
}
if err != nil {
return nil, err
}
return p, nil
}
type builderAndGoHash struct {
builder, goHash string
}
// A Commit describes an individual commit in a package.
//
// Each Commit entity is a descendant of its associated Package entity.
// In other words, all Commits with the same PackagePath belong to the same
// datastore entity group.
type Commit struct {
PackagePath string // (empty for main repo commits)
Hash string
// ResultData is the Data string of each build Result for this Commit.
// For non-Go commits, only the Results for the current Go tip, weekly,
// and release Tags are stored here. This is purely de-normalized data.
// The complete data set is stored in Result entities.
ResultData []string `datastore:",noindex"`
}
func (com *Commit) Key() *datastore.Key {
if com.Hash == "" {
panic("tried Key on Commit with empty Hash")
}
p := Package{Path: com.PackagePath}
key := com.PackagePath + "|" + com.Hash
return dsKey("Commit", key, p.Key())
}
// Valid reports whether the commit is valid.
func (c *Commit) Valid() bool {
// Valid really just means the hash is populated.
return validHash(c.Hash)
}
// each result line is approx 105 bytes. This constant is a tradeoff between
// build history and the AppEngine datastore limit of 1mb.
const maxResults = 1000
// AddResult adds the denormalized Result data to the Commit's
// ResultData field.
func (com *Commit) AddResult(tx *datastore.Transaction, r *Result) error {
err := tx.Get(com.Key(), com)
if err == datastore.ErrNoSuchEntity {
// If it doesn't exist, we create it below.
} else {
err = filterDatastoreError(err)
if err != nil {
return fmt.Errorf("Commit.AddResult, getting Commit: %v", err)
}
}
var resultExists bool
for i, s := range com.ResultData {
// if there already exists result data for this builder at com, overwrite it.
if strings.HasPrefix(s, r.Builder+"|") && strings.HasSuffix(s, "|"+r.GoHash) {
resultExists = true
com.ResultData[i] = r.Data()
}
}
if !resultExists {
// otherwise, add the new result data for this builder.
com.ResultData = trim(append(com.ResultData, r.Data()), maxResults)
}
if !com.Valid() {
return errors.New("putting Commit: commit is not valid")
}
if _, err := tx.Put(com.Key(), com); err != nil {
return fmt.Errorf("putting Commit: %v", err)
}
return nil
}
// removeResult removes the denormalized Result data from the ResultData field
// for the given builder and go hash.
// It must be called from within the datastore transaction that gets and puts
// the Commit. Note this is slightly different to AddResult, above.
func (com *Commit) RemoveResult(r *Result) {
var rd []string
for _, s := range com.ResultData {
if strings.HasPrefix(s, r.Builder+"|") && strings.HasSuffix(s, "|"+r.GoHash) {
continue
}
rd = append(rd, s)
}
com.ResultData = rd
}
func trim(s []string, n int) []string {
l := min(len(s), n)
return s[len(s)-l:]
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// Result returns the build Result for this Commit for the given builder/goHash.
//
// For the main Go repo, goHash is the empty string.
func (c *Commit) Result(builder, goHash string) *Result {
return result(c.ResultData, c.Hash, c.PackagePath, builder, goHash)
}
// Result returns the build Result for this commit for the given builder/goHash.
//
// For the main Go repo, goHash is the empty string.
func (c *CommitInfo) Result(builder, goHash string) *Result {
if r := result(c.ResultData, c.Hash, c.PackagePath, builder, goHash); r != nil {
return r
}
if u, ok := c.BuildingURLs[builderAndGoHash{builder, goHash}]; ok {
return &Result{
Builder: builder,
BuildingURL: u,
Hash: c.Hash,
GoHash: goHash,
}
}
if *fakeResults {
switch rand.Intn(3) {
default:
return nil
case 1:
return &Result{
Builder: builder,
Hash: c.Hash,
GoHash: goHash,
OK: true,
}
case 2:
return &Result{
Builder: builder,
Hash: c.Hash,
GoHash: goHash,
LogHash: "fakefailureurl",
}
}
}
return nil
}
func result(resultData []string, hash, packagePath, builder, goHash string) *Result {
for _, r := range resultData {
if !strings.HasPrefix(r, builder) {
// Avoid strings.SplitN alloc in the common case.
continue
}
p := strings.SplitN(r, "|", 4)
if len(p) != 4 || p[0] != builder || p[3] != goHash {
continue
}
return partsToResult(hash, packagePath, p)
}
return nil
}
// isUntested reports whether a cell in the build.golang.org grid is
// an untested configuration.
//
// repo is "go", "net", etc.
// branch is the branch of repo "master" or "release-branch.go1.12"
// goBranch applies only if repo != "go" and is of form "master" or "release-branch.go1.N"
//
// As a special case, "tip" is an alias for "master", since this app
// still uses a bunch of hg terms from when we used hg.
func isUntested(builder, repo, branch, goBranch string) bool {
if branch == "tip" {
branch = "master"
}
if goBranch == "tip" {
goBranch = "master"
}
bc, ok := dashboard.Builders[builder]
if !ok {
// Unknown builder, so not tested.
return true
}
return !bc.BuildsRepoPostSubmit(repo, branch, goBranch)
}
// knownIssue returns a known issue for the named builder,
// or zero if there isn't a known issue.
func knownIssue(builder string) int {
bc, ok := dashboard.Builders[builder]
if !ok {
// Unknown builder.
return 0
}
return bc.KnownIssue
}
// Results returns the build Results for this Commit.
func (c *CommitInfo) Results() (results []*Result) {
for _, r := range c.ResultData {
p := strings.SplitN(r, "|", 4)
if len(p) != 4 {
continue
}
results = append(results, partsToResult(c.Hash, c.PackagePath, p))
}
return
}
// ResultGoHashes, for non-go repos, returns the list of Go hashes that
// this repo has been (or should be) built at.
//
// For the main Go repo it always returns a slice with 1 element: the
// empty string.
func (c *CommitInfo) ResultGoHashes() []string {
// For the main repo, just return the empty string
// (there's no corresponding main repo hash for a main repo Commit).
// This function is only really useful for sub-repos.
if c.PackagePath == "" {
return []string{""}
}
var hashes []string
for _, r := range c.ResultData {
p := strings.SplitN(r, "|", 4)
if len(p) != 4 {
continue
}
// Append only new results (use linear scan to preserve order).
if !contains(hashes, p[3]) {
hashes = append(hashes, p[3])
}
}
// Return results in reverse order (newest first).
reverse(hashes)
return hashes
}
func contains(t []string, s string) bool {
for _, s2 := range t {
if s2 == s {
return true
}
}
return false
}
func reverse(s []string) {
for i := 0; i < len(s)/2; i++ {
j := len(s) - i - 1
s[i], s[j] = s[j], s[i]
}
}
// partsToResult creates a Result from ResultData substrings.
func partsToResult(hash, packagePath string, p []string) *Result {
return &Result{
Builder: p[0],
Hash: hash,
PackagePath: packagePath,
GoHash: p[3],
OK: p[1] == "true",
LogHash: p[2],
}
}
// A Result describes a build result for a Commit on an OS/architecture.
//
// Each Result entity is a descendant of its associated Package entity.
type Result struct {
Builder string // "os-arch[-note]"
PackagePath string // (empty for Go commits, else "golang.org/x/foo")
Hash string
// The Go Commit this was built against (when PackagePath != ""; empty for Go commits).
GoHash string
BuildingURL string `datastore:"-"` // non-empty if currently building
OK bool
Log string `datastore:"-"` // for JSON unmarshaling only
LogHash string `datastore:",noindex"` // Key to the Log record.
RunTime int64 // time to build+test in nanoseconds
}
func (r *Result) Key() *datastore.Key {
p := Package{Path: r.PackagePath}
key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash
return dsKey("Result", key, p.Key())
}
func (r *Result) Valid() error {
if !validHash(r.Hash) {
return errors.New("invalid Hash")
}
if r.PackagePath != "" && !validHash(r.GoHash) {
return errors.New("invalid GoHash")
}
return nil
}
// Data returns the Result in string format
// to be stored in Commit's ResultData field.
func (r *Result) Data() string {
return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
}
// A Log is a gzip-compressed log file stored under the SHA1 hash of the
// uncompressed log text.
type Log struct {
CompressedLog []byte `datastore:",noindex"`
}
func (l *Log) Text() ([]byte, error) {
d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog))
if err != nil {
return nil, fmt.Errorf("reading log data: %v", err)
}
b, err := ioutil.ReadAll(d)
if err != nil {
return nil, fmt.Errorf("reading log data: %v", err)
}
return b, nil
}
func PutLog(c context.Context, text string) (hash string, err error) {
b := new(bytes.Buffer)
z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
io.WriteString(z, text)
z.Close()
hash = loghash.New(text)
key := dsKey("Log", hash, nil)
_, err = datastoreClient.Put(c, key, &Log{b.Bytes()})
return
}