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 }