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  }