| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. |
| 4 |
| 5 package buildbucket |
| 6 |
| 7 import ( |
| 8 "encoding/json" |
| 9 "fmt" |
| 10 "net/url" |
| 11 "os" |
| 12 "path" |
| 13 "path/filepath" |
| 14 "strconv" |
| 15 "strings" |
| 16 "time" |
| 17 |
| 18 "golang.org/x/net/context" |
| 19 "google.golang.org/api/googleapi" |
| 20 |
| 21 "github.com/luci/luci-go/appengine/cmd/milo/resp" |
| 22 "github.com/luci/luci-go/common/api/buildbucket/buildbucket/v1" |
| 23 "github.com/luci/luci-go/common/clock" |
| 24 "github.com/luci/luci-go/common/errors" |
| 25 log "github.com/luci/luci-go/common/logging" |
| 26 "github.com/luci/luci-go/common/parallel" |
| 27 "github.com/luci/luci-go/common/retry" |
| 28 ) |
| 29 |
| 30 // search executes the search request with retries and exponential back-off. |
| 31 func search(c context.Context, client *buildbucket.Service, req *buildbucket.Sea
rchCall) ( |
| 32 *buildbucket.ApiSearchResponseMessage, error) { |
| 33 |
| 34 var res *buildbucket.ApiSearchResponseMessage |
| 35 err := retry.Retry( |
| 36 c, |
| 37 retry.TransientOnly(retry.Default), |
| 38 func() error { |
| 39 var err error |
| 40 res, err = req.Do() |
| 41 if apiErr, ok := err.(*googleapi.Error); ok && apiErr.Co
de >= 500 { |
| 42 err = errors.WrapTransient(apiErr) |
| 43 } |
| 44 return err |
| 45 }, |
| 46 func(err error, wait time.Duration) { |
| 47 log.WithError(err).Warningf(c, "buildbucket search reque
st failed transiently, will retry in %s", wait) |
| 48 }) |
| 49 return res, err |
| 50 } |
| 51 |
| 52 // fetchBuilds fetches builds given a criteria. |
| 53 // The returned builds are sorted by build creation descending. |
| 54 // count defines maximum number of builds to fetch; if <0, defaults to 100. |
| 55 func fetchBuilds(c context.Context, client *buildbucket.Service, bucket, builder
, |
| 56 status string, count int) ([]*buildbucket.ApiBuildMessage, error) { |
| 57 |
| 58 req := client.Search() |
| 59 req.Bucket(bucket) |
| 60 req.Status(status) |
| 61 req.Tag("builder:" + builder) |
| 62 |
| 63 if count < 0 { |
| 64 count = 100 |
| 65 } |
| 66 |
| 67 fetched := make([]*buildbucket.ApiBuildMessage, 0, count) |
| 68 start := clock.Now(c) |
| 69 for len(fetched) < count { |
| 70 req.MaxBuilds(int64(count - len(fetched))) |
| 71 |
| 72 res, err := search(c, client, req) |
| 73 switch { |
| 74 case err != nil: |
| 75 return fetched, err |
| 76 case res.Error != nil: |
| 77 return fetched, fmt.Errorf(res.Error.Message) |
| 78 } |
| 79 |
| 80 fetched = append(fetched, res.Builds...) |
| 81 |
| 82 if len(res.Builds) == 0 || res.NextCursor == "" { |
| 83 break |
| 84 } |
| 85 req.StartCursor(res.NextCursor) |
| 86 } |
| 87 log.Debugf(c, "Fetched %d %s builds in %s", len(fetched), status, clock.
Since(c, start)) |
| 88 return fetched, nil |
| 89 } |
| 90 |
| 91 // toMiloBuild converts a buildbucket build to a milo build. |
| 92 // In case of an error, returns a build with a description of the error |
| 93 // and logs the error. |
| 94 func toMiloBuild(c context.Context, build *buildbucket.ApiBuildMessage) *resp.Bu
ildSummary { |
| 95 // Parsing of parameters and result details is best effort. |
| 96 var params buildParameters |
| 97 if err := json.NewDecoder(strings.NewReader(build.ParametersJson)).Decod
e(¶ms); err != nil { |
| 98 log.Errorf(c, "Could not parse parameters of build %d: %s", buil
d.Id, err) |
| 99 } |
| 100 var resultDetails resultDetails |
| 101 if err := json.NewDecoder(strings.NewReader(build.ResultDetailsJson)).De
code(&resultDetails); err != nil { |
| 102 log.Errorf(c, "Could not parse result details of build %d: %s",
build.Id, err) |
| 103 } |
| 104 |
| 105 result := &resp.BuildSummary{ |
| 106 Text: []string{fmt.Sprintf("buildbucket id %d", build.Id)}, |
| 107 Revision: resultDetails.Properties.GotRevision, |
| 108 } |
| 109 if result.Revision == "" { |
| 110 result.Revision = params.Properties.Revision |
| 111 } |
| 112 |
| 113 var err error |
| 114 result.Status, err = parseStatus(build) |
| 115 if err != nil { |
| 116 // almost never happens |
| 117 log.WithError(err).Errorf(c, "could not convert status of build
%d", build.Id) |
| 118 result.Status = resp.InfraFailure |
| 119 result.Text = append(result.Text, fmt.Sprintf("invalid build: %s
", err)) |
| 120 } |
| 121 |
| 122 result.PendingTime.Started = parseTimestamp(build.CreatedTs) |
| 123 switch build.Status { |
| 124 case "SCHEDULED": |
| 125 result.PendingTime.Duration = clock.Since(c, result.PendingTime.
Started) |
| 126 |
| 127 case "STARTED": |
| 128 result.ExecutionTime.Started = parseTimestamp(build.StatusChange
dTs) |
| 129 result.ExecutionTime.Duration = clock.Since(c, result.PendingTim
e.Started) |
| 130 result.PendingTime.Finished = result.ExecutionTime.Started |
| 131 result.PendingTime.Duration = result.PendingTime.Finished.Sub(re
sult.PendingTime.Started) |
| 132 |
| 133 case "COMPLETED": |
| 134 // buildbucket does not provide build start time or execution du
ration. |
| 135 result.ExecutionTime.Finished = parseTimestamp(build.CompletedTs
) |
| 136 } |
| 137 |
| 138 cl := getChangeList(build, ¶ms, &resultDetails) |
| 139 if cl != nil { |
| 140 result.Blame = []*resp.Commit{cl} |
| 141 } |
| 142 |
| 143 tags := ParseTags(build.Tags) |
| 144 |
| 145 if build.Url != "" { |
| 146 u := build.Url |
| 147 parsed, err := url.Parse(u) |
| 148 |
| 149 // map milo links to itself |
| 150 switch { |
| 151 case err != nil: |
| 152 log.Errorf(c, "invalid URL in build %d: %s", build.Id, e
rr) |
| 153 case parsed.Host == "luci-milo.appspot.com": |
| 154 parsed.Host = "" |
| 155 parsed.Scheme = "" |
| 156 u = parsed.String() |
| 157 } |
| 158 |
| 159 result.Link = &resp.Link{ |
| 160 URL: u, |
| 161 Label: strconv.FormatInt(build.Id, 10), |
| 162 } |
| 163 |
| 164 // compute the best link label |
| 165 if taskID := tags["swarming_task_id"]; taskID != "" { |
| 166 result.Link.Label = taskID |
| 167 } else if resultDetails.Properties.BuildNumber != 0 { |
| 168 result.Link.Label = strconv.Itoa(resultDetails.Propertie
s.BuildNumber) |
| 169 } else if parsed != nil { |
| 170 // does the URL look like a buildbot build URL? |
| 171 pattern := fmt.Sprintf( |
| 172 `/%s/builders/%s/builds/`, |
| 173 strings.TrimPrefix(build.Bucket, "master."), par
ams.BuilderName) |
| 174 beforeBuildNumber, buildNumberStr := path.Split(parsed.P
ath) |
| 175 _, err := strconv.Atoi(buildNumberStr) |
| 176 if strings.HasSuffix(beforeBuildNumber, pattern) && err
== nil { |
| 177 result.Link.Label = buildNumberStr |
| 178 } |
| 179 } |
| 180 } |
| 181 |
| 182 return result |
| 183 } |
| 184 |
| 185 func getDebugBuilds(c context.Context, bucket, builder string, maxCompletedBuild
s int, target *resp.Builder) error { |
| 186 resFile, err := os.Open(filepath.Join("testdata", "buildbucket", bucket,
builder+".json")) |
| 187 if err != nil { |
| 188 return err |
| 189 } |
| 190 defer resFile.Close() |
| 191 |
| 192 res := &buildbucket.ApiSearchResponseMessage{} |
| 193 if err := json.NewDecoder(resFile).Decode(res); err != nil { |
| 194 return err |
| 195 } |
| 196 |
| 197 for _, bb := range res.Builds { |
| 198 mb := toMiloBuild(c, bb) |
| 199 switch mb.Status { |
| 200 case resp.NotRun: |
| 201 target.PendingBuilds = append(target.PendingBuilds, mb) |
| 202 |
| 203 case resp.Running: |
| 204 target.CurrentBuilds = append(target.CurrentBuilds, mb) |
| 205 |
| 206 case resp.Success, resp.Failure, resp.InfraFailure, resp.Warning
: |
| 207 if len(target.FinishedBuilds) < maxCompletedBuilds { |
| 208 target.FinishedBuilds = append(target.FinishedBu
ilds, mb) |
| 209 } |
| 210 |
| 211 default: |
| 212 panic("impossible") |
| 213 } |
| 214 } |
| 215 return nil |
| 216 } |
| 217 |
| 218 // builderImpl is the implementation for getting a milo builder page from buildb
ucket. |
| 219 // if maxCompletedBuilds < 0, 25 is used. |
| 220 func builderImpl(c context.Context, server, bucket, builder string, maxCompleted
Builds int) (*resp.Builder, error) { |
| 221 if maxCompletedBuilds < 0 { |
| 222 maxCompletedBuilds = 20 |
| 223 } |
| 224 |
| 225 result := &resp.Builder{ |
| 226 Name: builder, |
| 227 } |
| 228 if server == "debug" { |
| 229 return result, getDebugBuilds(c, bucket, builder, maxCompletedBu
ilds, result) |
| 230 } |
| 231 client, err := newClient(c, server) |
| 232 if err != nil { |
| 233 return nil, err |
| 234 } |
| 235 |
| 236 fetch := func(target *[]*resp.BuildSummary, status string, count int) er
ror { |
| 237 builds, err := fetchBuilds(c, client, bucket, builder, status, c
ount) |
| 238 if err != nil { |
| 239 log.Errorf(c, "Could not fetch builds with status %s: %s
", status, err) |
| 240 return err |
| 241 } |
| 242 *target = make([]*resp.BuildSummary, len(builds)) |
| 243 for i, bb := range builds { |
| 244 (*target)[i] = toMiloBuild(c, bb) |
| 245 } |
| 246 return nil |
| 247 } |
| 248 // fetch pending, current and finished builds concurrently. |
| 249 // Why not a single request? Because we need different build number |
| 250 // limits for different statuses. |
| 251 return result, parallel.FanOutIn(func(work chan<- func() error) { |
| 252 work <- func() error { |
| 253 return fetch(&result.PendingBuilds, StatusScheduled, -1) |
| 254 } |
| 255 work <- func() error { |
| 256 return fetch(&result.CurrentBuilds, StatusStarted, -1) |
| 257 } |
| 258 work <- func() error { |
| 259 return fetch(&result.FinishedBuilds, StatusCompleted, ma
xCompletedBuilds) |
| 260 } |
| 261 }) |
| 262 } |
| 263 |
| 264 // parseTimestamp converts buildbucket timestamp in microseconds to time.Time |
| 265 func parseTimestamp(microseconds int64) time.Time { |
| 266 if microseconds == 0 { |
| 267 return time.Time{} |
| 268 } |
| 269 return time.Unix(microseconds/1e6, microseconds%1e6*1000).UTC() |
| 270 } |
| 271 |
| 272 type newBuildsFirst []*resp.BuildSummary |
| 273 |
| 274 func (a newBuildsFirst) Len() int { return len(a) } |
| 275 func (a newBuildsFirst) Swap(i, j int) { a[i], a[j] = a[j], a[i] } |
| 276 func (a newBuildsFirst) Less(i, j int) bool { |
| 277 return a[i].PendingTime.Started.After(a[j].PendingTime.Started) |
| 278 } |
| OLD | NEW |