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 buildbot | |
6 | |
7 import ( | |
8 "crypto/sha1" | |
9 "encoding/base64" | |
10 "errors" | |
11 "fmt" | |
12 "sort" | |
13 "strings" | |
14 "time" | |
15 | |
16 "github.com/luci/gae/service/datastore" | |
17 "github.com/luci/gae/service/memcache" | |
18 | |
19 "github.com/luci/luci-go/common/clock" | |
20 "github.com/luci/luci-go/common/logging" | |
21 "github.com/luci/luci-go/milo/api/resp" | |
22 "golang.org/x/net/context" | |
23 ) | |
24 | |
25 // builderRef is used for keying specific builds in a master json. | |
26 type builderRef struct { | |
27 builder string | |
28 buildNum int | |
29 } | |
30 | |
31 // buildMap contains all of the current build within a master json. We use this | |
32 // because buildbot returns all current builds as within the slaves portion, whe
reas | |
33 // it's eaiser to map thenm by builders instead. | |
34 type buildMap map[builderRef]*buildbotBuild | |
35 | |
36 // mergeText merges buildbot summary texts, which sometimes separates | |
37 // words that should be merged together, this combines them into a single | |
38 // line. | |
39 func mergeText(text []string) []string { | |
40 result := make([]string, 0, len(text)) | |
41 merge := false | |
42 for _, line := range text { | |
43 if merge { | |
44 merge = false | |
45 result[len(result)-1] += " " + line | |
46 continue | |
47 } | |
48 result = append(result, line) | |
49 switch line { | |
50 case "build", "failed", "exception": | |
51 merge = true | |
52 default: | |
53 merge = false | |
54 } | |
55 } | |
56 | |
57 // We can remove error messages about the step "steps" if it's part of a
longer | |
58 // message because this step is an artifact of running on recipes and it
's | |
59 // not important to users. | |
60 if len(result) > 1 { | |
61 switch result[0] { | |
62 case "failed steps", "exception steps": | |
63 result = result[1:] | |
64 } | |
65 } | |
66 return result | |
67 } | |
68 | |
69 func getBuildSummary(b *buildbotBuild) *resp.BuildSummary { | |
70 started, finished, duration := parseTimes(nil, b.Times) | |
71 return &resp.BuildSummary{ | |
72 Link: resp.NewLink(fmt.Sprintf("#%d", b.Number), fmt.Sprintf("
%d", b.Number)), | |
73 Status: b.toStatus(), | |
74 ExecutionTime: resp.Interval{ | |
75 Started: started, | |
76 Finished: finished, | |
77 Duration: duration, | |
78 }, | |
79 Text: mergeText(b.Text), | |
80 Blame: blame(b), | |
81 Revision: b.Sourcestamp.Revision, | |
82 } | |
83 } | |
84 | |
85 // getBuilds fetches all of the recent builds from the . Note that | |
86 // getBuilds() does not perform ACL checks. | |
87 func getBuilds( | |
88 c context.Context, masterName, builderName string, finished bool, limit
int, cursor *datastore.Cursor) ( | |
89 []*resp.BuildSummary, *datastore.Cursor, error) { | |
90 | |
91 // TODO(hinoka): Builder specific structs. | |
92 result := []*resp.BuildSummary{} | |
93 q := datastore.NewQuery("buildbotBuild") | |
94 q = q.Eq("finished", finished) | |
95 q = q.Eq("master", masterName) | |
96 q = q.Eq("builder", builderName) | |
97 q = q.Order("-number") | |
98 if cursor != nil { | |
99 q = q.Start(*cursor) | |
100 } | |
101 buildbots, nextCursor, err := runBuildsQuery(c, q, int32(limit)) | |
102 if err != nil { | |
103 return nil, nil, err | |
104 } | |
105 for _, b := range buildbots { | |
106 result = append(result, getBuildSummary(b)) | |
107 } | |
108 return result, nextCursor, nil | |
109 } | |
110 | |
111 // maybeSetGetCursor is a cheesy way to implement bidirectional paging with forw
ard-only | |
112 // datastore cursor by creating a mapping of nextCursor -> thisCursor | |
113 // in memcache. maybeSetGetCursor stores the future mapping, then returns prevC
ursor | |
114 // in the mapping for thisCursor -> prevCursor, if available. | |
115 func maybeSetGetCursor(c context.Context, thisCursor, nextCursor *datastore.Curs
or, limit int) (*datastore.Cursor, bool) { | |
116 key := func(c datastore.Cursor) string { | |
117 // Memcache key limit is 250 bytes, hash our cursor to get under
this limit. | |
118 blob := sha1.Sum([]byte(c.String())) | |
119 return fmt.Sprintf("v2:cursors:buildbot_builders:%d:%s", limit,
base64.StdEncoding.EncodeToString(blob[:])) | |
120 } | |
121 // Set the next cursor to this cursor mapping, if available. | |
122 if nextCursor != nil { | |
123 item := memcache.NewItem(c, key(*nextCursor)) | |
124 if thisCursor == nil { | |
125 // Make sure we know it exists, just empty | |
126 item.SetValue([]byte{}) | |
127 } else { | |
128 item.SetValue([]byte((*thisCursor).String())) | |
129 } | |
130 item.SetExpiration(24 * time.Hour) | |
131 memcache.Set(c, item) | |
132 } | |
133 // Try to get the last cursor, if valid and available. | |
134 if thisCursor == nil { | |
135 return nil, false | |
136 } | |
137 if item, err := memcache.GetKey(c, key(*thisCursor)); err == nil { | |
138 if len(item.Value()) == 0 { | |
139 return nil, true | |
140 } | |
141 if prevCursor, err := datastore.DecodeCursor(c, string(item.Valu
e())); err == nil { | |
142 return &prevCursor, true | |
143 } | |
144 } | |
145 return nil, false | |
146 } | |
147 | |
148 var errMasterNotFound = errors.New( | |
149 "Either the request resource was not found or you have insufficient perm
issions") | |
150 var errNotAuth = errors.New("You are not authenticated, try logging in") | |
151 | |
152 type errBuilderNotFound struct { | |
153 master string | |
154 builder string | |
155 available []string | |
156 } | |
157 | |
158 func (e errBuilderNotFound) Error() string { | |
159 avail := strings.Join(e.available, "\n") | |
160 return fmt.Sprintf("Cannot find builder %q in master %q.\nAvailable buil
ders: \n%s", | |
161 e.builder, e.master, avail) | |
162 } | |
163 | |
164 func summarizeSlavePool( | |
165 baseURL string, slaves []string, slaveMap map[string]*buildbotSlave) *re
sp.MachinePool { | |
166 | |
167 mp := &resp.MachinePool{ | |
168 Total: len(slaves), | |
169 Bots: make([]resp.Bot, 0, len(slaves)), | |
170 } | |
171 for _, slaveName := range slaves { | |
172 slave, ok := slaveMap[slaveName] | |
173 bot := resp.Bot{ | |
174 Name: *resp.NewLink( | |
175 slaveName, | |
176 fmt.Sprintf("%s/buildslaves/%s", baseURL, slaveN
ame), | |
177 ), | |
178 } | |
179 switch { | |
180 case !ok: | |
181 // This shouldn't happen | |
182 case !slave.Connected: | |
183 bot.Status = resp.Disconnected | |
184 mp.Disconnected++ | |
185 case len(slave.RunningbuildsMap) > 0: | |
186 bot.Status = resp.Busy | |
187 mp.Busy++ | |
188 default: | |
189 bot.Status = resp.Idle | |
190 mp.Idle++ | |
191 } | |
192 mp.Bots = append(mp.Bots, bot) | |
193 } | |
194 return mp | |
195 } | |
196 | |
197 // builderImpl is the implementation for getting a milo builder page from buildb
ot. | |
198 // This gets: | |
199 // * Current Builds from querying the master json from the datastore. | |
200 // * Recent Builds from a cron job that backfills the recent builds. | |
201 func builderImpl( | |
202 c context.Context, masterName, builderName string, limit int, cursor str
ing) ( | |
203 *resp.Builder, error) { | |
204 | |
205 var thisCursor *datastore.Cursor | |
206 if cursor != "" { | |
207 tmpCur, err := datastore.DecodeCursor(c, cursor) | |
208 if err != nil { | |
209 return nil, fmt.Errorf("bad cursor: %s", err) | |
210 } | |
211 thisCursor = &tmpCur | |
212 } | |
213 | |
214 result := &resp.Builder{ | |
215 Name: builderName, | |
216 } | |
217 master, internal, t, err := getMasterJSON(c, masterName) | |
218 if err != nil { | |
219 return nil, err | |
220 } | |
221 if clock.Now(c).Sub(t) > 2*time.Minute { | |
222 warning := fmt.Sprintf( | |
223 "WARNING: Master data is stale (last updated %s)", t) | |
224 logging.Warningf(c, warning) | |
225 result.Warning = warning | |
226 } | |
227 | |
228 p, ok := master.Builders[builderName] | |
229 if !ok { | |
230 // This long block is just to return a good error message when a
n invalid | |
231 // buildbot builder is specified. | |
232 keys := make([]string, 0, len(master.Builders)) | |
233 for k := range master.Builders { | |
234 keys = append(keys, k) | |
235 } | |
236 sort.Strings(keys) | |
237 return nil, errBuilderNotFound{masterName, builderName, keys} | |
238 } | |
239 // Extract pending builds out of the master json. | |
240 result.PendingBuilds = make([]*resp.BuildSummary, len(p.PendingBuildStat
es)) | |
241 logging.Debugf(c, "Number of pending builds: %d", len(p.PendingBuildStat
es)) | |
242 for i, pb := range p.PendingBuildStates { | |
243 start := time.Unix(int64(pb.SubmittedAt), 0).UTC() | |
244 result.PendingBuilds[i] = &resp.BuildSummary{ | |
245 PendingTime: resp.Interval{ | |
246 Started: start, | |
247 Duration: clock.Now(c).UTC().Sub(start), | |
248 }, | |
249 } | |
250 result.PendingBuilds[i].Blame = make([]*resp.Commit, len(pb.Sour
ce.Changes)) | |
251 for j, cm := range pb.Source.Changes { | |
252 result.PendingBuilds[i].Blame[j] = &resp.Commit{ | |
253 AuthorEmail: cm.Who, | |
254 CommitURL: cm.Revlink, | |
255 } | |
256 } | |
257 } | |
258 | |
259 baseURL := "https://build.chromium.org/p/" | |
260 if internal { | |
261 baseURL = "https://uberchromegw.corp.google.com/i/" | |
262 } | |
263 result.MachinePool = summarizeSlavePool(baseURL+master.Name, p.Slaves, m
aster.Slaves) | |
264 | |
265 // This is CPU bound anyways, so there's no need to do this in parallel. | |
266 finishedBuilds, nextCursor, err := getBuilds(c, masterName, builderName,
true, limit, thisCursor) | |
267 if err != nil { | |
268 return nil, err | |
269 } | |
270 if prevCursor, ok := maybeSetGetCursor(c, thisCursor, nextCursor, limit)
; ok { | |
271 if prevCursor == nil { | |
272 // Magic string to signal display prev without cursor | |
273 result.PrevCursor = "EMPTY" | |
274 } else { | |
275 result.PrevCursor = (*prevCursor).String() | |
276 } | |
277 } | |
278 if nextCursor != nil { | |
279 result.NextCursor = (*nextCursor).String() | |
280 } | |
281 // Cursor is not needed for current builds. | |
282 currentBuilds, _, err := getBuilds(c, masterName, builderName, false, 0,
nil) | |
283 if err != nil { | |
284 return nil, err | |
285 } | |
286 // currentBuilds is presented in reversed order, so flip it | |
287 for i, j := 0, len(currentBuilds)-1; i < j; i, j = i+1, j-1 { | |
288 currentBuilds[i], currentBuilds[j] = currentBuilds[j], currentBu
ilds[i] | |
289 } | |
290 result.CurrentBuilds = currentBuilds | |
291 | |
292 for _, fb := range finishedBuilds { | |
293 if fb != nil { | |
294 result.FinishedBuilds = append(result.FinishedBuilds, fb
) | |
295 } | |
296 } | |
297 return result, nil | |
298 } | |
OLD | NEW |