blob: 6c5cc9d55f3e21b6a852ef275d66f641721850a3 [file] [log] [blame]
// Copyright 2021 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 userhtml
import (
"context"
"fmt"
"sort"
"time"
"github.com/dustin/go-humanize"
"golang.org/x/sync/errgroup"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/templates"
"go.chromium.org/luci/cv/internal/acls"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/run"
"go.chromium.org/luci/cv/internal/tryjob"
)
func runDetails(c *router.Context) {
ctx := c.Context
rID := fmt.Sprintf("%s/%s", c.Params.ByName("Project"), c.Params.ByName("Run"))
// Load the Run, checking its existence and ACLs.
r, err := run.LoadRun(ctx, common.RunID(rID), acls.NewRunReadChecker())
if err != nil {
errPage(c, err)
return
}
cls, latestTryjobs, logs, err := loadRunInfo(ctx, r)
if err != nil {
errPage(c, err)
return
}
// Compute next and previous runs for all cls in parallel.
clsAndLinks, err := computeCLsAndLinks(ctx, cls, r.ID)
if err != nil {
errPage(c, err)
return
}
templates.MustRender(ctx, c.Writer, "pages/run_details.html", templates.Args{
"Run": r,
"Logs": logs,
"Cls": clsAndLinks,
"RelTime": func(ts time.Time) string {
return humanize.RelTime(ts, startTime(ctx), "ago", "from now")
},
"LatestTryjobs": latestTryjobs,
})
}
func loadRunInfo(ctx context.Context, r *run.Run) ([]*run.RunCL, []*uiTryjob, []*uiLogEntry, error) {
var cls []*run.RunCL
var latestTryjobs []*tryjob.Tryjob
var runLogs []*run.LogEntry
var tryjobLogs []*tryjob.ExecutionLogEntry
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() (err error) {
cls, err = run.LoadRunCLs(ctx, common.RunID(r.ID), r.CLs)
if err != nil {
return err
}
// Sort a stack of CLs by external ID.
sort.Slice(cls, func(i, j int) bool { return cls[i].ExternalID < cls[j].ExternalID })
return nil
})
eg.Go(func() (err error) {
runLogs, err = run.LoadRunLogEntries(ctx, r.ID)
return err
})
eg.Go(func() (err error) {
tryjobLogs, err = tryjob.LoadExecutionLogs(ctx, r.ID)
return err
})
eg.Go(func() (err error) {
if r.UseCVTryjobExecutor {
for _, execution := range r.Tryjobs.GetState().GetExecutions() {
// TODO(yiwzhang): display the tryjob as not-started even if no
// attempt has been triggered.
if attempt := tryjob.LatestAttempt(execution); attempt != nil {
latestTryjobs = append(latestTryjobs, &tryjob.Tryjob{
ID: common.TryjobID(attempt.GetTryjobId()),
})
}
}
if err := datastore.Get(ctx, latestTryjobs); err != nil {
return errors.Annotate(err, "failed to load tryjobs").Tag(transient.Tag).Err()
}
} else {
for _, tj := range r.Tryjobs.GetTryjobs() {
launchedBy := r.ID
if tj.Reused {
launchedBy = ""
}
latestTryjobs = append(latestTryjobs, &tryjob.Tryjob{
ExternalID: tryjob.ExternalID(tj.ExternalId),
Definition: tj.Definition,
Status: tj.Status,
Result: tj.Result,
LaunchedBy: launchedBy,
})
}
}
return nil
})
if err := eg.Wait(); err != nil {
return nil, nil, nil, err
}
uiLogEntries := make([]*uiLogEntry, len(runLogs)+len(tryjobLogs))
for i, rl := range runLogs {
uiLogEntries[i] = &uiLogEntry{
runLog: rl,
run: r,
cls: cls,
}
}
offset := len(runLogs)
for i, tl := range tryjobLogs {
uiLogEntries[offset+i] = &uiLogEntry{
tryjobLog: tl,
run: r,
cls: cls,
}
}
// ensure most recent log is at the top of the list.
sort.Slice(uiLogEntries, func(i, j int) bool {
return uiLogEntries[i].Time().After(uiLogEntries[j].Time())
})
return cls, makeUITryjobs(latestTryjobs, r.ID), uiLogEntries, nil
}
type clAndNeighborRuns struct {
Prev, Next common.RunID
CL *run.RunCL
}
func (cl *clAndNeighborRuns) URLWithPatchset() string {
return fmt.Sprintf("%s/%d", cl.CL.ExternalID.MustURL(), cl.CL.Detail.GetPatchset())
}
func (cl *clAndNeighborRuns) ShortWithPatchset() string {
return fmt.Sprintf("%s/%d", displayCLExternalID(cl.CL.ExternalID), cl.CL.Detail.GetPatchset())
}
func computeCLsAndLinks(ctx context.Context, cls []*run.RunCL, rid common.RunID) ([]*clAndNeighborRuns, error) {
ret := make([]*clAndNeighborRuns, len(cls))
eg, ctx := errgroup.WithContext(ctx)
for i, cl := range cls {
i, cl := i, cl
eg.Go(func() error {
prev, next, err := getNeighborsByCL(ctx, cl, rid)
if err != nil {
return errors.Annotate(err, "unable to get previous and next runs for cl %s", cl.ExternalID).Err()
}
ret[i] = &clAndNeighborRuns{
Prev: prev,
Next: next,
CL: cl,
}
return nil
})
}
return ret, eg.Wait()
}
func getNeighborsByCL(ctx context.Context, cl *run.RunCL, rID common.RunID) (common.RunID, common.RunID, error) {
eg, ctx := errgroup.WithContext(ctx)
prev := common.RunID("")
eg.Go(func() error {
qb := run.CLQueryBuilder{CLID: cl.ID, Limit: 1}.BeforeInProject(rID)
switch keys, err := qb.GetAllRunKeys(ctx); {
case err != nil:
return err
case len(keys) == 1:
prev = common.RunID(keys[0].StringID())
// It's OK to return the prev Run ID w/o checking ACLs because even if the
// user can't see the prev Run, user is already served this Run's ID, which
// contains the same LUCI project.
if prev.LUCIProject() != rID.LUCIProject() {
panic(fmt.Errorf("CLQueryBuilder.Before didn't limit project: %q vs %q", prev, rID))
}
}
return nil
})
next := common.RunID("")
eg.Go(func() error {
// CLQueryBuilder gives Runs of the same project ordered from newest to
// oldest. We need the oldest run which was created after the `rID`.
// So, fetch all the Run IDs, and choose the last of them.
//
// In practice, there should be << 100 Runs per CL, but since we are
// fetching just the keys and the query is very cheap, we go to 500.
const limit = 500
qb := run.CLQueryBuilder{CLID: cl.ID, Limit: limit}.AfterInProject(rID)
switch keys, err := qb.GetAllRunKeys(ctx); {
case err != nil:
return err
case len(keys) == limit:
return errors.Reason("too many Runs (>=%d) after %q", limit, rID).Err()
case len(keys) > 0:
next = common.RunID(keys[len(keys)-1].StringID())
// It's OK to return the next Run ID w/o checking ACLs because even if the
// user can't see the next Run, user is already served this Run's ID, which
// contains the same LUCI project.
if next.LUCIProject() != rID.LUCIProject() {
panic(fmt.Errorf("CLQueryBuilder.After didn't limit project: %q vs %q", next, rID))
}
}
return nil
})
err := eg.Wait()
return prev, next, err
}