blob: 9db32ddbcc3d41ce31da84fb5d045b81e8e27e4d [file] [log] [blame]
package buildbot
import (
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/golang/glog"
"skia.googlesource.com/buildbot.git/go/gitinfo"
)
var (
// TODO(borenet): Avoid hard-coding this list. Instead, obtain it from
// checked-in code or the set of masters which are actually running.
MASTER_NAMES = []string{"client.skia", "client.skia.android", "client.skia.compile", "client.skia.fyi"}
httpGet = http.Get
)
// get loads data from a buildbot JSON endpoint.
func get(url string, rv interface{}) error {
resp, err := httpGet(url)
if err != nil {
return fmt.Errorf("Failed to GET %s: %v", url, err)
}
dec := json.NewDecoder(resp.Body)
if err := dec.Decode(rv); err != nil {
return fmt.Errorf("Failed to decode JSON: %v", err)
}
return nil
}
// findCommitsRecursive is a recursive function called by findCommitsForBuild.
// It traces the history to find builds which were first included in the given
// build.
func findCommitsRecursive(b *Build, hash string, repo *gitinfo.GitInfo) ([]string, error) {
// Shortcut for empty hashes. This can happen when a commit has no
// parents (initial commit) or when a Build has no GotRevision.
if hash == "" {
return []string{}, nil
}
// Determine whether any build already includes this commit.
n, err := GetBuildForCommit(b.MasterName, b.BuilderName, hash)
if err != nil {
return nil, fmt.Errorf("Could not find build for commit %s: %v", hash, err)
}
// If so, stop.
if n >= 0 {
return []string{}, nil
}
// Recurse on the commit's parents.
c, err := repo.Details(hash)
if err != nil {
return nil, fmt.Errorf("Failed to obtain details for %s: %v", hash, err)
}
commits := []string{hash}
for _, p := range c.Parents {
moreCommits, err := findCommitsRecursive(b, p, repo)
if err != nil {
return nil, err
}
commits = append(commits, moreCommits...)
}
return commits, nil
}
// findCommitsForBuild determines which commits were first included in the
// given build. Assumes that all previous builds for the given builder/master
// are already in the database.
func findCommitsForBuild(b *Build, repo *gitinfo.GitInfo) ([]string, error) {
// Shortcut for the first build for a given builder: this build must be
// the first inclusion for all revisions prior to b.GotRevision.
if b.Number == 0 && b.GotRevision != "" {
return repo.RevList(b.GotRevision)
}
// Start tracing commits back in time until we hit a previous build.
return findCommitsRecursive(b, b.GotRevision, repo)
}
// getBuildFromMaster retrieves the given build from the build master's JSON
// interface as specified by the master, builder, and build number.
func getBuildFromMaster(master, builder string, buildNumber int, repo *gitinfo.GitInfo) (*Build, error) {
var build Build
url := fmt.Sprintf("%s%s/json/builders/%s/builds/%d", BUILDBOT_URL, master, builder, buildNumber)
err := get(url, &build)
if err != nil {
return nil, fmt.Errorf("Failed to retrieve build #%v for %v: %v", buildNumber, builder, err)
}
build.Branch = build.branch()
build.GotRevision = build.gotRevision()
build.MasterName = master
slaveProp := build.GetProperty("slavename").([]interface{})
if slaveProp != nil && len(slaveProp) == 3 {
build.BuildSlave = slaveProp[1].(string)
}
build.Started = build.Times[0]
build.Finished = build.Times[1]
propBytes, err := json.Marshal(&build.Properties)
if err != nil {
return nil, fmt.Errorf("Unable to convert build properties to JSON: %v", err)
}
build.PropertiesStr = string(propBytes)
// Set the master, builder, and buildNumber props on each step.
for _, s := range build.Steps {
s.MasterName = build.MasterName
s.BuilderName = build.BuilderName
s.BuildNumber = build.Number
if len(s.ResultsRaw) > 0 {
if s.ResultsRaw[0] == nil {
s.ResultsRaw[0] = 0.0
}
s.Results = int(s.ResultsRaw[0].(float64))
} else {
s.Results = 0
}
s.Started = s.Times[0]
s.Finished = s.Times[1]
}
// Find the commits for this build.
commits, err := findCommitsForBuild(&build, repo)
if err != nil {
return nil, fmt.Errorf("Could not find commits for build: %v", err)
}
build.Commits = commits
return &build, nil
}
// retryGetBuildFromMaster retrieves the given build from the build master's JSON
// interface as specified by the master, builder, and build number. Makes
// multiple attempts in case the master fails to respond.
func retryGetBuildFromMaster(master, builder string, buildNumber int, repo *gitinfo.GitInfo) (*Build, error) {
var b *Build
var err error
for attempt := 0; attempt < 3; attempt++ {
b, err = getBuildFromMaster(master, builder, buildNumber, repo)
if err == nil {
break
}
time.Sleep(500 * time.Millisecond)
}
return b, err
}
// IngestBuild retrieves the given build from the build master's JSON interface
// and pushes it into the database.
func IngestBuild(master, builder string, buildNumber int, repo *gitinfo.GitInfo) error {
b, err := retryGetBuildFromMaster(master, builder, buildNumber, repo)
if err != nil {
return fmt.Errorf("Failed to load build from master: %v", err)
}
return b.ReplaceIntoDB()
}
// getLatestBuilds returns a map whose keys are master names and values are
// sub-maps whose keys are builder names and values are build numbers
// representing the newest build for each builder/master pair.
func getLatestBuilds() (map[string]map[string]int, error) {
res := map[string]map[string]int{}
errs := map[string]error{}
type builder struct {
CachedBuilds []int
}
var wg sync.WaitGroup
for _, master := range MASTER_NAMES {
wg.Add(1)
go func(m string) {
defer wg.Done()
builders := map[string]*builder{}
err := get(BUILDBOT_URL+m+"/json/builders", &builders)
if err != nil {
errs[m] = fmt.Errorf("Failed to retrieve builders for %v: %v", m, err)
return
}
myMap := map[string]int{}
for name, b := range builders {
if len(b.CachedBuilds) > 0 {
myMap[name] = b.CachedBuilds[len(b.CachedBuilds)-1]
}
}
if len(myMap) > 0 {
res[m] = myMap
}
}(master)
}
wg.Wait()
if len(errs) != 0 {
return nil, fmt.Errorf("Encountered errors while loading builder data from masters: %v", errs)
}
return res, nil
}
// getUningestedBuilds returns a map whose keys are master names and values are
// sub-maps whose keys are builder names and values are slices of ints
// representing the numbers of builds which have not yet been ingested.
func getUningestedBuilds() (map[string]map[string][]int, error) {
// Get the latest and last-processed builds for all builders.
latest, err := getLatestBuilds()
if err != nil {
return nil, fmt.Errorf("Failed to get latest builds: %v", err)
}
lastProcessed, err := getLastProcessedBuilds()
if err != nil {
return nil, fmt.Errorf("Failed to get last-processed builds: %v", err)
}
// Find the range of uningested builds for each builder.
type numRange struct {
Start int // The last-ingested build number.
End int // The latest build number.
}
ranges := map[string]map[string]*numRange{}
for _, b := range lastProcessed {
if _, ok := ranges[b.MasterName]; !ok {
ranges[b.MasterName] = map[string]*numRange{}
}
ranges[b.MasterName][b.BuilderName] = &numRange{
Start: b.Number,
End: b.Number,
}
}
for m, v := range latest {
if _, ok := ranges[m]; !ok {
ranges[m] = map[string]*numRange{}
}
for b, n := range v {
if _, ok := ranges[m][b]; !ok {
ranges[m][b] = &numRange{
Start: -1,
End: n,
}
} else {
ranges[m][b].End = n
}
}
}
// Create a slice of build numbers for the uningested builds.
unprocessed := map[string]map[string][]int{}
for m, v := range ranges {
masterMap := map[string][]int{}
for b, r := range v {
builds := make([]int, r.End-r.Start)
for i := r.Start + 1; i <= r.End; i++ {
builds[i-r.Start-1] = i
}
if len(builds) > 0 {
masterMap[b] = builds
}
}
if len(masterMap) > 0 {
unprocessed[m] = masterMap
}
}
return unprocessed, nil
}
// IngestNewBuilds finds the set of uningested builds and ingests them.
func IngestNewBuilds(repo *gitinfo.GitInfo) error {
// TODO(borenet): Investigate the use of channels here. We should be
// able to start ingesting builds as the data becomes available rather
// than waiting until the end.
buildsToProcess, err := getUningestedBuilds()
if err != nil {
return fmt.Errorf("Failed to obtain the set of uningested builds: %v", err)
}
unfinished, err := getUnfinishedBuilds()
if err != nil {
return fmt.Errorf("Failed to obtain the set of unfinished builds: %v", err)
}
for _, b := range unfinished {
if _, ok := buildsToProcess[b.MasterName]; !ok {
buildsToProcess[b.MasterName] = map[string][]int{}
}
if _, ok := buildsToProcess[b.BuilderName]; !ok {
buildsToProcess[b.MasterName][b.BuilderName] = []int{}
}
buildsToProcess[b.MasterName][b.BuilderName] = append(buildsToProcess[b.MasterName][b.BuilderName], b.Number)
}
// TODO(borenet): Figure out how much of this is safe to parallelize.
// We can definitely do different masters in parallel, and maybe we can
// ingest different builders in parallel as well.
var wg sync.WaitGroup
errors := map[string]error{}
for m, v := range buildsToProcess {
wg.Add(1)
go func(master string, buildsToProcessForMaster map[string][]int) {
defer wg.Done()
for b, w := range buildsToProcessForMaster {
for _, n := range w {
glog.Infof("Ingesting build: %s, %s, %d", master, b, n)
if err := IngestBuild(master, b, n, repo); err != nil {
errors[master] = fmt.Errorf("Failed to ingest build: %v", err)
}
}
}
}(m, v)
}
wg.Wait()
if len(errors) > 0 {
return fmt.Errorf("Errors: %v", errors)
}
return nil
}