github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/dashboard/app/build/notify.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // +build appengine 6 7 package build 8 9 import ( 10 "bytes" 11 "encoding/gob" 12 "errors" 13 "fmt" 14 "io/ioutil" 15 "net/http" 16 "net/url" 17 "regexp" 18 "runtime" 19 "sort" 20 "text/template" 21 22 "appengine" 23 "appengine/datastore" 24 "appengine/delay" 25 "appengine/mail" 26 "appengine/urlfetch" 27 ) 28 29 const ( 30 mailFrom = "builder@golang.org" // use this for sending any mail 31 failMailTo = "golang-dev@googlegroups.com" 32 domain = "build.golang.org" 33 gobotBase = "http://research.swtch.com/gobot_codereview" 34 ) 35 36 // ignoreFailure is a set of builders that we don't email about because 37 // they are not yet production-ready. 38 var ignoreFailure = map[string]bool{ 39 "dragonfly-386": true, 40 "dragonfly-amd64": true, 41 "freebsd-arm": true, 42 "netbsd-amd64-bsiegert": true, 43 "netbsd-arm-rpi": true, 44 "plan9-amd64-aram": true, 45 } 46 47 // notifyOnFailure checks whether the supplied Commit or the subsequent 48 // Commit (if present) breaks the build for this builder. 49 // If either of those commits break the build an email notification is sent 50 // from a delayed task. (We use a task because this way the mail won't be 51 // sent if the enclosing datastore transaction fails.) 52 // 53 // This must be run in a datastore transaction, and the provided *Commit must 54 // have been retrieved from the datastore within that transaction. 55 func notifyOnFailure(c appengine.Context, com *Commit, builder string) error { 56 if ignoreFailure[builder] { 57 return nil 58 } 59 60 // TODO(adg): implement notifications for packages 61 if com.PackagePath != "" { 62 return nil 63 } 64 65 p := &Package{Path: com.PackagePath} 66 var broken *Commit 67 cr := com.Result(builder, "") 68 if cr == nil { 69 return fmt.Errorf("no result for %s/%s", com.Hash, builder) 70 } 71 q := datastore.NewQuery("Commit").Ancestor(p.Key(c)) 72 if cr.OK { 73 // This commit is OK. Notify if next Commit is broken. 74 next := new(Commit) 75 q = q.Filter("ParentHash=", com.Hash) 76 if err := firstMatch(c, q, next); err != nil { 77 if err == datastore.ErrNoSuchEntity { 78 // OK at tip, no notification necessary. 79 return nil 80 } 81 return err 82 } 83 if nr := next.Result(builder, ""); nr != nil && !nr.OK { 84 c.Debugf("commit ok: %#v\nresult: %#v", com, cr) 85 c.Debugf("next commit broken: %#v\nnext result:%#v", next, nr) 86 broken = next 87 } 88 } else { 89 // This commit is broken. Notify if the previous Commit is OK. 90 prev := new(Commit) 91 q = q.Filter("Hash=", com.ParentHash) 92 if err := firstMatch(c, q, prev); err != nil { 93 if err == datastore.ErrNoSuchEntity { 94 // No previous result, let the backfill of 95 // this result trigger the notification. 96 return nil 97 } 98 return err 99 } 100 if pr := prev.Result(builder, ""); pr != nil && pr.OK { 101 c.Debugf("commit broken: %#v\nresult: %#v", com, cr) 102 c.Debugf("previous commit ok: %#v\nprevious result:%#v", prev, pr) 103 broken = com 104 } 105 } 106 if broken == nil { 107 return nil 108 } 109 r := broken.Result(builder, "") 110 if r == nil { 111 return fmt.Errorf("finding result for %q: %+v", builder, com) 112 } 113 return commonNotify(c, broken, builder, r.LogHash) 114 } 115 116 // firstMatch executes the query q and loads the first entity into v. 117 func firstMatch(c appengine.Context, q *datastore.Query, v interface{}) error { 118 t := q.Limit(1).Run(c) 119 _, err := t.Next(v) 120 if err == datastore.Done { 121 err = datastore.ErrNoSuchEntity 122 } 123 return err 124 } 125 126 var notifyLater = delay.Func("notify", notify) 127 128 // notify tries to update the CL for the given Commit with a failure message. 129 // If it doesn't succeed, it sends a failure email to golang-dev. 130 func notify(c appengine.Context, com *Commit, builder, logHash string) { 131 v := url.Values{"brokebuild": {builder}, "log": {logHash}} 132 if !updateCL(c, com, v) { 133 // Send a mail notification if the CL can't be found. 134 sendFailMail(c, com, builder, logHash) 135 } 136 } 137 138 // updateCL tells gobot to update the CL for the given Commit with 139 // the provided query values. 140 func updateCL(c appengine.Context, com *Commit, v url.Values) bool { 141 cl, err := lookupCL(c, com) 142 if err != nil { 143 c.Errorf("could not find CL for %v: %v", com.Hash, err) 144 return false 145 } 146 u := fmt.Sprintf("%v?cl=%v&%s", gobotBase, cl, v.Encode()) 147 r, err := urlfetch.Client(c).Post(u, "text/plain", nil) 148 if err != nil { 149 c.Errorf("could not update CL %v: %v", cl, err) 150 return false 151 } 152 r.Body.Close() 153 if r.StatusCode != http.StatusOK { 154 c.Errorf("could not update CL %v: %v", cl, r.Status) 155 return false 156 } 157 return true 158 } 159 160 var clURL = regexp.MustCompile(`https://codereview.appspot.com/([0-9]+)`) 161 162 // lookupCL consults code.google.com for the full change description for the 163 // provided Commit, and returns the relevant CL number. 164 func lookupCL(c appengine.Context, com *Commit) (string, error) { 165 url := "https://code.google.com/p/go/source/detail?r=" + com.Hash 166 r, err := urlfetch.Client(c).Get(url) 167 if err != nil { 168 return "", err 169 } 170 defer r.Body.Close() 171 if r.StatusCode != http.StatusOK { 172 return "", fmt.Errorf("retrieving %v: %v", url, r.Status) 173 } 174 b, err := ioutil.ReadAll(r.Body) 175 if err != nil { 176 return "", err 177 } 178 m := clURL.FindAllSubmatch(b, -1) 179 if m == nil { 180 return "", errors.New("no CL URL found on changeset page") 181 } 182 // Return the last visible codereview URL on the page, 183 // in case the change description refers to another CL. 184 return string(m[len(m)-1][1]), nil 185 } 186 187 var sendFailMailTmpl = template.Must(template.New("notify.txt"). 188 Funcs(template.FuncMap(tmplFuncs)). 189 ParseFiles("build/notify.txt")) 190 191 func init() { 192 gob.Register(&Commit{}) // for delay 193 } 194 195 var ( 196 sendPerfMailLater = delay.Func("sendPerfMail", sendPerfMailFunc) 197 sendPerfMailTmpl = template.Must( 198 template.New("perf_notify.txt"). 199 Funcs(template.FuncMap(tmplFuncs)). 200 ParseFiles("build/perf_notify.txt"), 201 ) 202 ) 203 204 // MUST be called from inside a transaction. 205 func sendPerfFailMail(c appengine.Context, builder string, res *PerfResult) error { 206 com := &Commit{Hash: res.CommitHash} 207 if err := datastore.Get(c, com.Key(c), com); err != nil { 208 return err 209 } 210 logHash := "" 211 parsed := res.ParseData() 212 for _, data := range parsed[builder] { 213 if !data.OK { 214 logHash = data.Artifacts["log"] 215 break 216 } 217 } 218 if logHash == "" { 219 return fmt.Errorf("can not find failed result for commit %v on builder %v", com.Hash, builder) 220 } 221 return commonNotify(c, com, builder, logHash) 222 } 223 224 // commonNotify MUST!!! be called from within a transaction inside which 225 // the provided Commit entity was retrieved from the datastore. 226 func commonNotify(c appengine.Context, com *Commit, builder, logHash string) error { 227 if com.Num == 0 || com.Desc == "" { 228 stk := make([]byte, 10000) 229 n := runtime.Stack(stk, false) 230 stk = stk[:n] 231 c.Errorf("refusing to notify with com=%+v\n%s", *com, string(stk)) 232 return fmt.Errorf("misuse of commonNotify") 233 } 234 if com.FailNotificationSent { 235 return nil 236 } 237 c.Infof("%s is broken commit; notifying", com.Hash) 238 notifyLater.Call(c, com, builder, logHash) // add task to queue 239 com.FailNotificationSent = true 240 return putCommit(c, com) 241 } 242 243 // sendFailMail sends a mail notification that the build failed on the 244 // provided commit and builder. 245 func sendFailMail(c appengine.Context, com *Commit, builder, logHash string) { 246 // get Log 247 k := datastore.NewKey(c, "Log", logHash, 0, nil) 248 l := new(Log) 249 if err := datastore.Get(c, k, l); err != nil { 250 c.Errorf("finding Log record %v: %v", logHash, err) 251 return 252 } 253 logText, err := l.Text() 254 if err != nil { 255 c.Errorf("unpacking Log record %v: %v", logHash, err) 256 return 257 } 258 259 // prepare mail message 260 var body bytes.Buffer 261 err = sendFailMailTmpl.Execute(&body, map[string]interface{}{ 262 "Builder": builder, "Commit": com, "LogHash": logHash, "LogText": logText, 263 "Hostname": domain, 264 }) 265 if err != nil { 266 c.Errorf("rendering mail template: %v", err) 267 return 268 } 269 subject := fmt.Sprintf("%s broken by %s", builder, shortDesc(com.Desc)) 270 msg := &mail.Message{ 271 Sender: mailFrom, 272 To: []string{failMailTo}, 273 ReplyTo: failMailTo, 274 Subject: subject, 275 Body: body.String(), 276 } 277 278 // send mail 279 if err := mail.Send(c, msg); err != nil { 280 c.Errorf("sending mail: %v", err) 281 } 282 } 283 284 type PerfChangeBenchmark struct { 285 Name string 286 Metrics []*PerfChangeMetric 287 } 288 289 type PerfChangeMetric struct { 290 Name string 291 Old uint64 292 New uint64 293 Delta float64 294 } 295 296 type PerfChangeBenchmarkSlice []*PerfChangeBenchmark 297 298 func (l PerfChangeBenchmarkSlice) Len() int { return len(l) } 299 func (l PerfChangeBenchmarkSlice) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 300 func (l PerfChangeBenchmarkSlice) Less(i, j int) bool { 301 b1, p1 := splitBench(l[i].Name) 302 b2, p2 := splitBench(l[j].Name) 303 if b1 != b2 { 304 return b1 < b2 305 } 306 return p1 < p2 307 } 308 309 type PerfChangeMetricSlice []*PerfChangeMetric 310 311 func (l PerfChangeMetricSlice) Len() int { return len(l) } 312 func (l PerfChangeMetricSlice) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 313 func (l PerfChangeMetricSlice) Less(i, j int) bool { return l[i].Name < l[j].Name } 314 315 func sendPerfMailFunc(c appengine.Context, com *Commit, prevCommitHash, builder string, changes []*PerfChange) { 316 // Sort the changes into the right order. 317 var benchmarks []*PerfChangeBenchmark 318 for _, ch := range changes { 319 // Find the benchmark. 320 var b *PerfChangeBenchmark 321 for _, b1 := range benchmarks { 322 if b1.Name == ch.Bench { 323 b = b1 324 break 325 } 326 } 327 if b == nil { 328 b = &PerfChangeBenchmark{Name: ch.Bench} 329 benchmarks = append(benchmarks, b) 330 } 331 b.Metrics = append(b.Metrics, &PerfChangeMetric{Name: ch.Metric, Old: ch.Old, New: ch.New, Delta: ch.Diff}) 332 } 333 for _, b := range benchmarks { 334 sort.Sort(PerfChangeMetricSlice(b.Metrics)) 335 } 336 sort.Sort(PerfChangeBenchmarkSlice(benchmarks)) 337 338 u := fmt.Sprintf("http://%v/perfdetail?commit=%v&commit0=%v&kind=builder&builder=%v", domain, com.Hash, prevCommitHash, builder) 339 340 // Prepare mail message (without Commit, for updateCL). 341 var body bytes.Buffer 342 err := sendPerfMailTmpl.Execute(&body, map[string]interface{}{ 343 "Builder": builder, "Hostname": domain, "Url": u, "Benchmarks": benchmarks, 344 }) 345 if err != nil { 346 c.Errorf("rendering perf mail template: %v", err) 347 return 348 } 349 350 // First, try to update the CL. 351 v := url.Values{"textmsg": {body.String()}} 352 if updateCL(c, com, v) { 353 return 354 } 355 356 // Otherwise, send mail (with Commit, for independent mail message). 357 body.Reset() 358 err = sendPerfMailTmpl.Execute(&body, map[string]interface{}{ 359 "Builder": builder, "Commit": com, "Hostname": domain, "Url": u, "Benchmarks": benchmarks, 360 }) 361 if err != nil { 362 c.Errorf("rendering perf mail template: %v", err) 363 return 364 } 365 subject := fmt.Sprintf("Perf changes on %s by %s", builder, shortDesc(com.Desc)) 366 msg := &mail.Message{ 367 Sender: mailFrom, 368 To: []string{failMailTo}, 369 ReplyTo: failMailTo, 370 Subject: subject, 371 Body: body.String(), 372 } 373 374 // send mail 375 if err := mail.Send(c, msg); err != nil { 376 c.Errorf("sending mail: %v", err) 377 } 378 }