blob: 01c7f4fa056bb9e7cc3799372ffbf496e2e02ca9 [file] [log] [blame]
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package buildsource
import (
"context"
"encoding/hex"
"fmt"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/sync/parallel"
"go.chromium.org/luci/milo/api/config"
"go.chromium.org/luci/milo/common"
"go.chromium.org/luci/milo/common/model"
"go.chromium.org/luci/milo/frontend/ui"
)
// ConsoleRow is one row of a particular console.
//
// It has the git commit for the row, as well as a mapping of column index to
// the Builds associated with it for this commit. The columns are defined by the
// order of the Builder messages in the Console config message (one column per
// Builder message).
//
// Builds is a map since most commit rows have a small subset of the available
// builders.
type ConsoleRow struct {
Commit string
Builds map[int][]*model.BuildSummary
}
// GetConsoleRows returns a row-oriented collection of BuildSummary
// objects. Each row corresponds to the similarly-indexed commit in the
// `commits` slice.
func GetConsoleRows(c context.Context, project string, console *config.Console, commits []string) ([]*ConsoleRow, error) {
rawCommits := make([][]byte, len(commits))
for i, c := range commits {
var err error
if rawCommits[i], err = hex.DecodeString(c); err != nil {
return nil, errors.Annotate(err, "bad commit[%d]: %q", i, c).Err()
}
}
// Maps all builderIDs to the indexes of the columns it appears in.
columnMap := map[string][]int{}
for columnIdx, b := range console.Builders {
columnMap[b.Name] = append(columnMap[b.Name], columnIdx)
}
ret := make([]*ConsoleRow, len(commits))
url := console.RepoUrl
// HACK(iannucci): This little hack should be removed when console definitions
// no longer use a manifest name of "REVISION". REVISION was used to index the
// 'got_revision' value before manifests were implemented.
if console.ManifestName == "REVISION" {
url = ""
}
partialKey := model.NewPartialManifestKey(project, console.Id, console.ManifestName, url)
q := datastore.NewQuery("BuildSummary")
err := parallel.WorkPool(4, func(ch chan<- func() error) {
for i := range rawCommits {
i := i
r := &ConsoleRow{Commit: commits[i]}
ret[i] = r
ch <- func() error {
fullQ := q.Eq("ManifestKeys", partialKey.AddRevision(rawCommits[i]))
return datastore.Run(c, fullQ, func(bs *model.BuildSummary) {
if bs.Experimental && !console.IncludeExperimentalBuilds {
return
}
if columnIdxs, ok := columnMap[bs.BuilderID]; ok {
if r.Builds == nil {
r.Builds = map[int][]*model.BuildSummary{}
}
for _, columnIdx := range columnIdxs {
r.Builds[columnIdx] = append(r.Builds[columnIdx], bs)
}
}
})
}
}
})
return ret, err
}
// GetConsoleSummariesFromDefs returns a map of consoleID -> summary from the
// datastore using a slice of console definitions as input.
//
// This expects all builders in all consoles coming from the same projectID.
func GetConsoleSummariesFromDefs(c context.Context, consoleEnts []*common.Console, projectID string) (
map[common.ConsoleID]*ui.BuilderSummaryGroup, error) {
// Maps consoleID -> console config definition.
consoles := make(map[common.ConsoleID]*config.Console, len(consoleEnts))
// Maps the BuilderID to the per-console pointer-to-summary in the summaries
// map. Note that builders with multiple builderIDs in the same console will
// all map to the same BuilderSummary.
columns := map[BuilderID]map[common.ConsoleID][]*model.BuilderSummary{}
// The return result.
summaries := map[common.ConsoleID]*ui.BuilderSummaryGroup{}
for _, ent := range consoleEnts {
cid := ent.ConsoleID()
consoles[cid] = &ent.Def
summaries[cid] = &ui.BuilderSummaryGroup{
Builders: make([]*model.BuilderSummary, len(ent.Def.Builders)),
Name: ui.NewLink(
ent.ID,
fmt.Sprintf("/p/%s/g/%s/console", ent.ProjectID(), ent.ID),
fmt.Sprintf("Console %s in project %s", ent.ID, ent.ProjectID()),
),
}
for i, column := range ent.Def.Builders {
s := &model.BuilderSummary{
BuilderID: column.Name,
ProjectID: projectID,
}
summaries[cid].Builders[i] = s
name := BuilderID(column.Name)
// Find/populate the BuilderID -> {console: summary}
colMap, ok := columns[name]
if !ok {
colMap = map[common.ConsoleID][]*model.BuilderSummary{}
columns[name] = colMap
}
colMap[cid] = append(colMap[cid], s)
}
}
// Now grab ALL THE DATA.
bs := make([]*model.BuilderSummary, 0, len(columns))
for builderID := range columns {
bs = append(bs, &model.BuilderSummary{
BuilderID: string(builderID),
// TODO: change builder ID format to include project id.
ProjectID: projectID,
})
}
if err := datastore.Get(c, bs); err != nil {
me := err.(errors.MultiError)
lme := errors.NewLazyMultiError(len(me))
for i, ierr := range me {
if ierr == datastore.ErrNoSuchEntity {
logging.Infof(c, "Missing builder: %s", bs[i].BuilderID)
ierr = nil // ignore ErrNoSuchEntity
bs[i] = nil // nil out the BuilderSummary, want to skip this below
}
lme.Assign(i, ierr)
}
// Return an error only if we encounter an error other than datastore.ErrNoSuchEntity.
if err := lme.Get(); err != nil {
return nil, err
}
}
// Now we have the mapping from BuilderID -> summaries, and ALL THE DATA, map
// the data back into the summaries.
for _, summary := range bs {
if summary == nil { // We got ErrNoSuchEntity above
continue
}
for cid, curSummaries := range columns[BuilderID(summary.BuilderID)] {
cons := consoles[cid]
// If this console doesn't show experimental builds, skip all summaries of
// experimental builds.
if !cons.IncludeExperimentalBuilds && summary.LastFinishedExperimental {
continue
}
for _, curSummary := range curSummaries {
// If the new summary's build was created before the current summary's
// build, skip it.
if summary.LastFinishedCreated.Before((*curSummary).LastFinishedCreated) {
continue
}
// Looks like this is the best summary for this slot so far, so save it.
*curSummary = *summary
}
}
}
return summaries, nil
}