go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/userhtml/run_details.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package userhtml
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"time"
    22  
    23  	"github.com/dustin/go-humanize"
    24  	"golang.org/x/sync/errgroup"
    25  
    26  	"go.chromium.org/luci/common/errors"
    27  	"go.chromium.org/luci/common/retry/transient"
    28  	"go.chromium.org/luci/gae/service/datastore"
    29  	"go.chromium.org/luci/server/router"
    30  	"go.chromium.org/luci/server/templates"
    31  
    32  	"go.chromium.org/luci/cv/internal/acls"
    33  	"go.chromium.org/luci/cv/internal/common"
    34  	"go.chromium.org/luci/cv/internal/run"
    35  	"go.chromium.org/luci/cv/internal/run/runquery"
    36  	"go.chromium.org/luci/cv/internal/tryjob"
    37  )
    38  
    39  func runDetails(c *router.Context) {
    40  	ctx := c.Request.Context()
    41  
    42  	rID := fmt.Sprintf("%s/%s", c.Params.ByName("Project"), c.Params.ByName("Run"))
    43  
    44  	// Load the Run, checking its existence and ACLs.
    45  	r, err := run.LoadRun(ctx, common.RunID(rID), acls.NewRunReadChecker())
    46  	if err != nil {
    47  		errPage(c, err)
    48  		return
    49  	}
    50  
    51  	cls, latestTryjobs, logs, err := loadRunInfo(ctx, r)
    52  	if err != nil {
    53  		errPage(c, err)
    54  		return
    55  	}
    56  
    57  	// Compute next and previous runs for all cls in parallel.
    58  	clsAndLinks, err := computeCLsAndLinks(ctx, cls, r.ID)
    59  	if err != nil {
    60  		errPage(c, err)
    61  		return
    62  	}
    63  
    64  	templates.MustRender(ctx, c.Writer, "pages/run_details.html", templates.Args{
    65  		"Run":  r,
    66  		"Logs": logs,
    67  		"Cls":  clsAndLinks,
    68  		"RelTime": func(ts time.Time) string {
    69  			return humanize.RelTime(ts, startTime(ctx), "ago", "from now")
    70  		},
    71  		"LatestTryjobs": latestTryjobs,
    72  	})
    73  }
    74  
    75  func loadRunInfo(ctx context.Context, r *run.Run) ([]*run.RunCL, []*uiTryjob, []*uiLogEntry, error) {
    76  	var cls []*run.RunCL
    77  	var latestTryjobs []*tryjob.Tryjob
    78  	var runLogs []*run.LogEntry
    79  	var tryjobLogs []*tryjob.ExecutionLogEntry
    80  	eg, ctx := errgroup.WithContext(ctx)
    81  	eg.Go(func() (err error) {
    82  		cls, err = run.LoadRunCLs(ctx, common.RunID(r.ID), r.CLs)
    83  		if err != nil {
    84  			return err
    85  		}
    86  		// Sort a stack of CLs by external ID.
    87  		sort.Slice(cls, func(i, j int) bool { return cls[i].ExternalID < cls[j].ExternalID })
    88  		return nil
    89  	})
    90  	eg.Go(func() (err error) {
    91  		runLogs, err = run.LoadRunLogEntries(ctx, r.ID)
    92  		return err
    93  	})
    94  	eg.Go(func() (err error) {
    95  		tryjobLogs, err = tryjob.LoadExecutionLogs(ctx, r.ID)
    96  		return err
    97  	})
    98  	eg.Go(func() (err error) {
    99  		if len(r.Tryjobs.GetState().GetExecutions()) > 0 {
   100  			// TODO(yiwzhang): backfill Tryjobs data reported from CQDaemon to
   101  			// Tryjobs.State.Executions
   102  			for _, execution := range r.Tryjobs.GetState().GetExecutions() {
   103  				// TODO(yiwzhang): display the tryjob as not-started even if no
   104  				// attempt has been triggered.
   105  				if attempt := tryjob.LatestAttempt(execution); attempt != nil {
   106  					latestTryjobs = append(latestTryjobs, &tryjob.Tryjob{
   107  						ID: common.TryjobID(attempt.GetTryjobId()),
   108  					})
   109  				}
   110  			}
   111  			if err := datastore.Get(ctx, latestTryjobs); err != nil {
   112  				return errors.Annotate(err, "failed to load tryjobs").Tag(transient.Tag).Err()
   113  			}
   114  		} else {
   115  			for _, tj := range r.Tryjobs.GetTryjobs() {
   116  				launchedBy := r.ID
   117  				if tj.Reused {
   118  					launchedBy = ""
   119  				}
   120  				latestTryjobs = append(latestTryjobs, &tryjob.Tryjob{
   121  					ExternalID: tryjob.ExternalID(tj.ExternalId),
   122  					Definition: tj.Definition,
   123  					Status:     tj.Status,
   124  					Result:     tj.Result,
   125  					LaunchedBy: launchedBy,
   126  				})
   127  			}
   128  		}
   129  		return nil
   130  	})
   131  	if err := eg.Wait(); err != nil {
   132  		return nil, nil, nil, err
   133  	}
   134  	uiLogEntries := make([]*uiLogEntry, len(runLogs)+len(tryjobLogs))
   135  	for i, rl := range runLogs {
   136  		uiLogEntries[i] = &uiLogEntry{
   137  			runLog: rl,
   138  			run:    r,
   139  			cls:    cls,
   140  		}
   141  	}
   142  	offset := len(runLogs)
   143  	for i, tl := range tryjobLogs {
   144  		uiLogEntries[offset+i] = &uiLogEntry{
   145  			tryjobLog: tl,
   146  			run:       r,
   147  			cls:       cls,
   148  		}
   149  	}
   150  	// ensure most recent log is at the top of the list.
   151  	sort.Slice(uiLogEntries, func(i, j int) bool {
   152  		return uiLogEntries[i].Time().After(uiLogEntries[j].Time())
   153  	})
   154  	return cls, makeUITryjobs(latestTryjobs, r.ID), uiLogEntries, nil
   155  }
   156  
   157  type clAndNeighborRuns struct {
   158  	Prev, Next common.RunID
   159  	CL         *run.RunCL
   160  }
   161  
   162  func (cl *clAndNeighborRuns) URLWithPatchset() string {
   163  	return fmt.Sprintf("%s/%d", cl.CL.ExternalID.MustURL(), cl.CL.Detail.GetPatchset())
   164  }
   165  
   166  func (cl *clAndNeighborRuns) ShortWithPatchset() string {
   167  	return fmt.Sprintf("%s/%d", displayCLExternalID(cl.CL.ExternalID), cl.CL.Detail.GetPatchset())
   168  }
   169  
   170  func computeCLsAndLinks(ctx context.Context, cls []*run.RunCL, rid common.RunID) ([]*clAndNeighborRuns, error) {
   171  	ret := make([]*clAndNeighborRuns, len(cls))
   172  	eg, ctx := errgroup.WithContext(ctx)
   173  	for i, cl := range cls {
   174  		i, cl := i, cl
   175  		eg.Go(func() error {
   176  			prev, next, err := getNeighborsByCL(ctx, cl, rid)
   177  			if err != nil {
   178  				return errors.Annotate(err, "unable to get previous and next runs for cl %s", cl.ExternalID).Err()
   179  			}
   180  			ret[i] = &clAndNeighborRuns{
   181  				Prev: prev,
   182  				Next: next,
   183  				CL:   cl,
   184  			}
   185  			return nil
   186  		})
   187  	}
   188  	return ret, eg.Wait()
   189  }
   190  
   191  func getNeighborsByCL(ctx context.Context, cl *run.RunCL, rID common.RunID) (common.RunID, common.RunID, error) {
   192  	eg, ctx := errgroup.WithContext(ctx)
   193  
   194  	prev := common.RunID("")
   195  	eg.Go(func() error {
   196  		qb := runquery.CLQueryBuilder{CLID: cl.ID, Limit: 1}.BeforeInProject(rID)
   197  		switch keys, err := qb.GetAllRunKeys(ctx); {
   198  		case err != nil:
   199  			return err
   200  		case len(keys) == 1:
   201  			prev = common.RunID(keys[0].StringID())
   202  			// It's OK to return the prev Run ID w/o checking ACLs because even if the
   203  			// user can't see the prev Run, user is already served this Run's ID, which
   204  			// contains the same LUCI project.
   205  			if prev.LUCIProject() != rID.LUCIProject() {
   206  				panic(fmt.Errorf("CLQueryBuilder.Before didn't limit project: %q vs %q", prev, rID))
   207  			}
   208  		}
   209  		return nil
   210  	})
   211  
   212  	next := common.RunID("")
   213  	eg.Go(func() error {
   214  		// CLQueryBuilder gives Runs of the same project ordered from newest to
   215  		// oldest. We need the oldest run which was created after the `rID`.
   216  		// So, fetch all the Run IDs, and choose the last of them.
   217  		//
   218  		// In practice, there should be << 100 Runs per CL, but since we are
   219  		// fetching just the keys and the query is very cheap, we go to 500.
   220  		const limit = 500
   221  		qb := runquery.CLQueryBuilder{CLID: cl.ID, Limit: limit}.AfterInProject(rID)
   222  		switch keys, err := qb.GetAllRunKeys(ctx); {
   223  		case err != nil:
   224  			return err
   225  		case len(keys) == limit:
   226  			return errors.Reason("too many Runs (>=%d) after %q", limit, rID).Err()
   227  		case len(keys) > 0:
   228  			next = common.RunID(keys[len(keys)-1].StringID())
   229  			// It's OK to return the next Run ID w/o checking ACLs because even if the
   230  			// user can't see the next Run, user is already served this Run's ID, which
   231  			// contains the same LUCI project.
   232  			if next.LUCIProject() != rID.LUCIProject() {
   233  				panic(fmt.Errorf("CLQueryBuilder.After didn't limit project: %q vs %q", next, rID))
   234  			}
   235  		}
   236  		return nil
   237  	})
   238  
   239  	err := eg.Wait()
   240  	return prev, next, err
   241  }