github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/dashboard/app/build/handler.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  	"crypto/hmac"
    12  	"crypto/md5"
    13  	"encoding/json"
    14  	"errors"
    15  	"fmt"
    16  	"io/ioutil"
    17  	"net/http"
    18  	"strconv"
    19  	"strings"
    20  	"unicode/utf8"
    21  
    22  	"appengine"
    23  	"appengine/datastore"
    24  
    25  	"cache"
    26  	"key"
    27  )
    28  
    29  const (
    30  	commitsPerPage = 30
    31  	watcherVersion = 3 // must match dashboard/watcher/watcher.go
    32  	builderVersion = 1 // must match dashboard/builder/http.go
    33  )
    34  
    35  // commitHandler retrieves commit data or records a new commit.
    36  //
    37  // For GET requests it returns a Commit value for the specified
    38  // packagePath and hash.
    39  //
    40  // For POST requests it reads a JSON-encoded Commit value from the request
    41  // body and creates a new Commit entity. It also updates the "tip" Tag for
    42  // each new commit at tip.
    43  //
    44  // This handler is used by a gobuilder process in -commit mode.
    45  func commitHandler(r *http.Request) (interface{}, error) {
    46  	c := contextForRequest(r)
    47  	com := new(Commit)
    48  
    49  	if r.Method == "GET" {
    50  		com.PackagePath = r.FormValue("packagePath")
    51  		com.Hash = r.FormValue("hash")
    52  		err := datastore.Get(c, com.Key(c), com)
    53  		if com.Num == 0 && com.Desc == "" {
    54  			// Perf builder might have written an incomplete Commit.
    55  			// Pretend it doesn't exist, so that we can get complete details.
    56  			err = datastore.ErrNoSuchEntity
    57  		}
    58  		if err != nil {
    59  			if err == datastore.ErrNoSuchEntity {
    60  				// This error string is special.
    61  				// The commit watcher expects it.
    62  				// Do not change it.
    63  				return nil, errors.New("Commit not found")
    64  			}
    65  			return nil, fmt.Errorf("getting Commit: %v", err)
    66  		}
    67  		if com.Num == 0 {
    68  			// Corrupt state which shouldn't happen but does.
    69  			// Return an error so builders' commit loops will
    70  			// be willing to retry submitting this commit.
    71  			return nil, errors.New("in datastore with zero Num")
    72  		}
    73  		if com.Desc == "" || com.User == "" {
    74  			// Also shouldn't happen, but at least happened
    75  			// once on a single commit when trying to fix data
    76  			// in the datastore viewer UI?
    77  			return nil, errors.New("missing field")
    78  		}
    79  		// Strip potentially large and unnecessary fields.
    80  		com.ResultData = nil
    81  		com.PerfResults = nil
    82  		return com, nil
    83  	}
    84  	if r.Method != "POST" {
    85  		return nil, errBadMethod(r.Method)
    86  	}
    87  	if !isMasterKey(c, r.FormValue("key")) {
    88  		return nil, errors.New("can only POST commits with master key")
    89  	}
    90  
    91  	// For now, the commit watcher doesn't support gccgo.
    92  	// TODO(adg,cmang): remove this exception when gccgo is supported.
    93  	if dashboardForRequest(r) != gccgoDash {
    94  		v, _ := strconv.Atoi(r.FormValue("version"))
    95  		if v != watcherVersion {
    96  			return nil, fmt.Errorf("rejecting POST from commit watcher; need version %v", watcherVersion)
    97  		}
    98  	}
    99  
   100  	// POST request
   101  	body, err := ioutil.ReadAll(r.Body)
   102  	r.Body.Close()
   103  	if err != nil {
   104  		return nil, fmt.Errorf("reading Body: %v", err)
   105  	}
   106  	if !bytes.Contains(body, needsBenchmarkingBytes) {
   107  		c.Warningf("old builder detected at %v", r.RemoteAddr)
   108  		return nil, fmt.Errorf("rejecting old builder request, body does not contain %s: %q", needsBenchmarkingBytes, body)
   109  	}
   110  	if err := json.Unmarshal(body, com); err != nil {
   111  		return nil, fmt.Errorf("unmarshaling body %q: %v", body, err)
   112  	}
   113  	com.Desc = limitStringLength(com.Desc, maxDatastoreStringLen)
   114  	if err := com.Valid(); err != nil {
   115  		return nil, fmt.Errorf("validating Commit: %v", err)
   116  	}
   117  	defer cache.Tick(c)
   118  	tx := func(c appengine.Context) error {
   119  		return addCommit(c, com)
   120  	}
   121  	return nil, datastore.RunInTransaction(c, tx, nil)
   122  }
   123  
   124  var needsBenchmarkingBytes = []byte(`"NeedsBenchmarking"`)
   125  
   126  // addCommit adds the Commit entity to the datastore and updates the tip Tag.
   127  // It must be run inside a datastore transaction.
   128  func addCommit(c appengine.Context, com *Commit) error {
   129  	var ec Commit // existing commit
   130  	isUpdate := false
   131  	err := datastore.Get(c, com.Key(c), &ec)
   132  	if err != nil && err != datastore.ErrNoSuchEntity {
   133  		return fmt.Errorf("getting Commit: %v", err)
   134  	}
   135  	if err == nil {
   136  		// Commit already in the datastore. Any fields different?
   137  		// If not, don't do anything.
   138  		changes := (com.Num != 0 && com.Num != ec.Num) ||
   139  			com.ParentHash != ec.ParentHash ||
   140  			com.Desc != ec.Desc ||
   141  			com.User != ec.User ||
   142  			!com.Time.Equal(ec.Time)
   143  		if !changes {
   144  			return nil
   145  		}
   146  		ec.ParentHash = com.ParentHash
   147  		ec.Desc = com.Desc
   148  		ec.User = com.User
   149  		if !com.Time.IsZero() {
   150  			ec.Time = com.Time
   151  		}
   152  		if com.Num != 0 {
   153  			ec.Num = com.Num
   154  		}
   155  		isUpdate = true
   156  		com = &ec
   157  	}
   158  	p, err := GetPackage(c, com.PackagePath)
   159  	if err != nil {
   160  		return fmt.Errorf("GetPackage: %v", err)
   161  	}
   162  	if com.Num == 0 {
   163  		// get the next commit number
   164  		com.Num = p.NextNum
   165  		p.NextNum++
   166  		if _, err := datastore.Put(c, p.Key(c), p); err != nil {
   167  			return fmt.Errorf("putting Package: %v", err)
   168  		}
   169  	} else if com.Num >= p.NextNum {
   170  		p.NextNum = com.Num + 1
   171  		if _, err := datastore.Put(c, p.Key(c), p); err != nil {
   172  			return fmt.Errorf("putting Package: %v", err)
   173  		}
   174  	}
   175  	// if this isn't the first Commit test the parent commit exists.
   176  	// The all zeros are returned by hg's p1node template for parentless commits.
   177  	if com.ParentHash != "" && com.ParentHash != "0000000000000000000000000000000000000000" && com.ParentHash != "0000" {
   178  		n, err := datastore.NewQuery("Commit").
   179  			Filter("Hash =", com.ParentHash).
   180  			Ancestor(p.Key(c)).
   181  			Count(c)
   182  		if err != nil {
   183  			return fmt.Errorf("testing for parent Commit: %v", err)
   184  		}
   185  		if n == 0 {
   186  			return errors.New("parent commit not found")
   187  		}
   188  	} else if com.Num != 1 {
   189  		// This is the first commit; fail if it is not number 1.
   190  		// (This will happen if we try to upload a new/different repo
   191  		// where there is already commit data. A bad thing to do.)
   192  		return errors.New("this package already has a first commit; aborting")
   193  	}
   194  	// update the tip Tag if this is the Go repo and this isn't on a release branch
   195  	if p.Path == "" && !strings.HasPrefix(com.Desc, "[") && !isUpdate {
   196  		t := &Tag{Kind: "tip", Hash: com.Hash}
   197  		if _, err = datastore.Put(c, t.Key(c), t); err != nil {
   198  			return fmt.Errorf("putting Tag: %v", err)
   199  		}
   200  	}
   201  	// put the Commit
   202  	if err = putCommit(c, com); err != nil {
   203  		return err
   204  	}
   205  	if com.NeedsBenchmarking {
   206  		// add to CommitRun
   207  		cr, err := GetCommitRun(c, com.Num)
   208  		if err != nil {
   209  			return err
   210  		}
   211  		if err = cr.AddCommit(c, com); err != nil {
   212  			return err
   213  		}
   214  		// create PerfResult
   215  		res := &PerfResult{CommitHash: com.Hash, CommitNum: com.Num}
   216  		if _, err := datastore.Put(c, res.Key(c), res); err != nil {
   217  			return fmt.Errorf("putting PerfResult: %v", err)
   218  		}
   219  		// Update perf todo if necessary.
   220  		if err = AddCommitToPerfTodo(c, com); err != nil {
   221  			return err
   222  		}
   223  	}
   224  	return nil
   225  }
   226  
   227  // tagHandler records a new tag. It reads a JSON-encoded Tag value from the
   228  // request body and updates the Tag entity for the Kind of tag provided.
   229  //
   230  // This handler is used by a gobuilder process in -commit mode.
   231  func tagHandler(r *http.Request) (interface{}, error) {
   232  	if r.Method != "POST" {
   233  		return nil, errBadMethod(r.Method)
   234  	}
   235  
   236  	t := new(Tag)
   237  	defer r.Body.Close()
   238  	if err := json.NewDecoder(r.Body).Decode(t); err != nil {
   239  		return nil, err
   240  	}
   241  	if err := t.Valid(); err != nil {
   242  		return nil, err
   243  	}
   244  	c := contextForRequest(r)
   245  	defer cache.Tick(c)
   246  	_, err := datastore.Put(c, t.Key(c), t)
   247  	return nil, err
   248  }
   249  
   250  // Todo is a todoHandler response.
   251  type Todo struct {
   252  	Kind string // "build-go-commit" or "build-package"
   253  	Data interface{}
   254  }
   255  
   256  // todoHandler returns the next action to be performed by a builder.
   257  // It expects "builder" and "kind" query parameters and returns a *Todo value.
   258  // Multiple "kind" parameters may be specified.
   259  func todoHandler(r *http.Request) (interface{}, error) {
   260  	c := contextForRequest(r)
   261  	now := cache.Now(c)
   262  	key := "build-todo-" + r.Form.Encode()
   263  	var todo *Todo
   264  	if cache.Get(r, now, key, &todo) {
   265  		return todo, nil
   266  	}
   267  	var err error
   268  	builder := r.FormValue("builder")
   269  	for _, kind := range r.Form["kind"] {
   270  		var com *Commit
   271  		switch kind {
   272  		case "build-go-commit":
   273  			com, err = buildTodo(c, builder, "", "")
   274  			if com != nil {
   275  				com.PerfResults = []string{}
   276  			}
   277  		case "build-package":
   278  			packagePath := r.FormValue("packagePath")
   279  			goHash := r.FormValue("goHash")
   280  			com, err = buildTodo(c, builder, packagePath, goHash)
   281  			if com != nil {
   282  				com.PerfResults = []string{}
   283  			}
   284  		case "benchmark-go-commit":
   285  			com, err = perfTodo(c, builder)
   286  		}
   287  		if com != nil || err != nil {
   288  			if com != nil {
   289  				// ResultData can be large and not needed on builder.
   290  				com.ResultData = []string{}
   291  			}
   292  			todo = &Todo{Kind: kind, Data: com}
   293  			break
   294  		}
   295  	}
   296  	if err == nil {
   297  		cache.Set(r, now, key, todo)
   298  	}
   299  	return todo, err
   300  }
   301  
   302  // buildTodo returns the next Commit to be built (or nil if none available).
   303  //
   304  // If packagePath and goHash are empty, it scans the first 20 Go Commits in
   305  // Num-descending order and returns the first one it finds that doesn't have a
   306  // Result for this builder.
   307  //
   308  // If provided with non-empty packagePath and goHash args, it scans the first
   309  // 20 Commits in Num-descending order for the specified packagePath and
   310  // returns the first that doesn't have a Result for this builder and goHash.
   311  func buildTodo(c appengine.Context, builder, packagePath, goHash string) (*Commit, error) {
   312  	p, err := GetPackage(c, packagePath)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  
   317  	t := datastore.NewQuery("Commit").
   318  		Ancestor(p.Key(c)).
   319  		Limit(commitsPerPage).
   320  		Order("-Num").
   321  		Run(c)
   322  	for {
   323  		com := new(Commit)
   324  		if _, err := t.Next(com); err == datastore.Done {
   325  			break
   326  		} else if err != nil {
   327  			return nil, err
   328  		}
   329  		if com.Result(builder, goHash) == nil {
   330  			return com, nil
   331  		}
   332  	}
   333  
   334  	// Nothing left to do if this is a package (not the Go tree).
   335  	if packagePath != "" {
   336  		return nil, nil
   337  	}
   338  
   339  	// If there are no Go tree commits left to build,
   340  	// see if there are any subrepo commits that need to be built at tip.
   341  	// If so, ask the builder to build a go tree at the tip commit.
   342  	// TODO(adg): do the same for "weekly" and "release" tags.
   343  
   344  	tag, err := GetTag(c, "tip")
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	// Check that this Go commit builds OK for this builder.
   350  	// If not, don't re-build as the subrepos will never get built anyway.
   351  	com, err := tag.Commit(c)
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  	if r := com.Result(builder, ""); r != nil && !r.OK {
   356  		return nil, nil
   357  	}
   358  
   359  	pkgs, err := Packages(c, "subrepo")
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	for _, pkg := range pkgs {
   364  		com, err := pkg.LastCommit(c)
   365  		if err != nil {
   366  			c.Warningf("%v: no Commit found: %v", pkg, err)
   367  			continue
   368  		}
   369  		if com.Result(builder, tag.Hash) == nil {
   370  			return tag.Commit(c)
   371  		}
   372  	}
   373  
   374  	return nil, nil
   375  }
   376  
   377  // perfTodo returns the next Commit to be benchmarked (or nil if none available).
   378  func perfTodo(c appengine.Context, builder string) (*Commit, error) {
   379  	p := &Package{}
   380  	todo := &PerfTodo{Builder: builder}
   381  	err := datastore.Get(c, todo.Key(c), todo)
   382  	if err != nil && err != datastore.ErrNoSuchEntity {
   383  		return nil, fmt.Errorf("fetching PerfTodo: %v", err)
   384  	}
   385  	if err == datastore.ErrNoSuchEntity {
   386  		todo, err = buildPerfTodo(c, builder)
   387  		if err != nil {
   388  			return nil, err
   389  		}
   390  	}
   391  	if len(todo.CommitNums) == 0 {
   392  		return nil, nil
   393  	}
   394  
   395  	// Have commit to benchmark, fetch it.
   396  	num := todo.CommitNums[len(todo.CommitNums)-1]
   397  	t := datastore.NewQuery("Commit").
   398  		Ancestor(p.Key(c)).
   399  		Filter("Num =", num).
   400  		Limit(1).
   401  		Run(c)
   402  	com := new(Commit)
   403  	if _, err := t.Next(com); err != nil {
   404  		return nil, err
   405  	}
   406  	if !com.NeedsBenchmarking {
   407  		return nil, fmt.Errorf("commit from perf todo queue is not intended for benchmarking")
   408  	}
   409  
   410  	// Remove benchmarks from other builders.
   411  	var benchs []string
   412  	for _, b := range com.PerfResults {
   413  		bb := strings.Split(b, "|")
   414  		if bb[0] == builder && bb[1] != "meta-done" {
   415  			benchs = append(benchs, bb[1])
   416  		}
   417  	}
   418  	com.PerfResults = benchs
   419  
   420  	return com, nil
   421  }
   422  
   423  // buildPerfTodo creates PerfTodo for the builder with all commits. In a transaction.
   424  func buildPerfTodo(c appengine.Context, builder string) (*PerfTodo, error) {
   425  	todo := &PerfTodo{Builder: builder}
   426  	tx := func(c appengine.Context) error {
   427  		err := datastore.Get(c, todo.Key(c), todo)
   428  		if err != nil && err != datastore.ErrNoSuchEntity {
   429  			return fmt.Errorf("fetching PerfTodo: %v", err)
   430  		}
   431  		if err == nil {
   432  			return nil
   433  		}
   434  		t := datastore.NewQuery("CommitRun").
   435  			Ancestor((&Package{}).Key(c)).
   436  			Order("-StartCommitNum").
   437  			Run(c)
   438  		var nums []int
   439  		var releaseNums []int
   440  	loop:
   441  		for {
   442  			cr := new(CommitRun)
   443  			if _, err := t.Next(cr); err == datastore.Done {
   444  				break
   445  			} else if err != nil {
   446  				return fmt.Errorf("scanning commit runs for perf todo: %v", err)
   447  			}
   448  			for i := len(cr.Hash) - 1; i >= 0; i-- {
   449  				if !cr.NeedsBenchmarking[i] || cr.Hash[i] == "" {
   450  					continue // There's nothing to see here. Move along.
   451  				}
   452  				num := cr.StartCommitNum + i
   453  				for k, v := range knownTags {
   454  					// Releases are benchmarked first, because they are important (and there are few of them).
   455  					if cr.Hash[i] == v {
   456  						releaseNums = append(releaseNums, num)
   457  						if k == "go1" {
   458  							break loop // Point of no benchmark: test/bench/shootout: update timing.log to Go 1.
   459  						}
   460  					}
   461  				}
   462  				nums = append(nums, num)
   463  			}
   464  		}
   465  		todo.CommitNums = orderPerfTodo(nums)
   466  		todo.CommitNums = append(todo.CommitNums, releaseNums...)
   467  		if _, err = datastore.Put(c, todo.Key(c), todo); err != nil {
   468  			return fmt.Errorf("putting PerfTodo: %v", err)
   469  		}
   470  		return nil
   471  	}
   472  	return todo, datastore.RunInTransaction(c, tx, nil)
   473  }
   474  
   475  func removeCommitFromPerfTodo(c appengine.Context, builder string, num int) error {
   476  	todo := &PerfTodo{Builder: builder}
   477  	err := datastore.Get(c, todo.Key(c), todo)
   478  	if err != nil && err != datastore.ErrNoSuchEntity {
   479  		return fmt.Errorf("fetching PerfTodo: %v", err)
   480  	}
   481  	if err == datastore.ErrNoSuchEntity {
   482  		return nil
   483  	}
   484  	for i := len(todo.CommitNums) - 1; i >= 0; i-- {
   485  		if todo.CommitNums[i] == num {
   486  			for ; i < len(todo.CommitNums)-1; i++ {
   487  				todo.CommitNums[i] = todo.CommitNums[i+1]
   488  			}
   489  			todo.CommitNums = todo.CommitNums[:i]
   490  			_, err = datastore.Put(c, todo.Key(c), todo)
   491  			if err != nil {
   492  				return fmt.Errorf("putting PerfTodo: %v", err)
   493  			}
   494  			break
   495  		}
   496  	}
   497  	return nil
   498  }
   499  
   500  // packagesHandler returns a list of the non-Go Packages monitored
   501  // by the dashboard.
   502  func packagesHandler(r *http.Request) (interface{}, error) {
   503  	kind := r.FormValue("kind")
   504  	c := contextForRequest(r)
   505  	now := cache.Now(c)
   506  	key := "build-packages-" + kind
   507  	var p []*Package
   508  	if cache.Get(r, now, key, &p) {
   509  		return p, nil
   510  	}
   511  	p, err := Packages(c, kind)
   512  	if err != nil {
   513  		return nil, err
   514  	}
   515  	cache.Set(r, now, key, p)
   516  	return p, nil
   517  }
   518  
   519  // resultHandler records a build result.
   520  // It reads a JSON-encoded Result value from the request body,
   521  // creates a new Result entity, and updates the relevant Commit entity.
   522  // If the Log field is not empty, resultHandler creates a new Log entity
   523  // and updates the LogHash field before putting the Commit entity.
   524  func resultHandler(r *http.Request) (interface{}, error) {
   525  	if r.Method != "POST" {
   526  		return nil, errBadMethod(r.Method)
   527  	}
   528  
   529  	// For now, the gccgo builders are using the old stuff.
   530  	// TODO(adg,cmang): remove this exception when gccgo is updated.
   531  	if dashboardForRequest(r) != gccgoDash {
   532  		v, _ := strconv.Atoi(r.FormValue("version"))
   533  		if v != builderVersion {
   534  			return nil, fmt.Errorf("rejecting POST from builder; need version %v", builderVersion)
   535  		}
   536  	}
   537  
   538  	c := contextForRequest(r)
   539  	res := new(Result)
   540  	defer r.Body.Close()
   541  	if err := json.NewDecoder(r.Body).Decode(res); err != nil {
   542  		return nil, fmt.Errorf("decoding Body: %v", err)
   543  	}
   544  	if err := res.Valid(); err != nil {
   545  		return nil, fmt.Errorf("validating Result: %v", err)
   546  	}
   547  	defer cache.Tick(c)
   548  	// store the Log text if supplied
   549  	if len(res.Log) > 0 {
   550  		hash, err := PutLog(c, res.Log)
   551  		if err != nil {
   552  			return nil, fmt.Errorf("putting Log: %v", err)
   553  		}
   554  		res.LogHash = hash
   555  	}
   556  	tx := func(c appengine.Context) error {
   557  		// check Package exists
   558  		if _, err := GetPackage(c, res.PackagePath); err != nil {
   559  			return fmt.Errorf("GetPackage: %v", err)
   560  		}
   561  		// put Result
   562  		if _, err := datastore.Put(c, res.Key(c), res); err != nil {
   563  			return fmt.Errorf("putting Result: %v", err)
   564  		}
   565  		// add Result to Commit
   566  		com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash}
   567  		if err := com.AddResult(c, res); err != nil {
   568  			return fmt.Errorf("AddResult: %v", err)
   569  		}
   570  		// Send build failure notifications, if necessary.
   571  		// Note this must run after the call AddResult, which
   572  		// populates the Commit's ResultData field.
   573  		return notifyOnFailure(c, com, res.Builder)
   574  	}
   575  	return nil, datastore.RunInTransaction(c, tx, nil)
   576  }
   577  
   578  // perf-result request payload
   579  type PerfRequest struct {
   580  	Builder   string
   581  	Benchmark string
   582  	Hash      string
   583  	OK        bool
   584  	Metrics   []PerfMetric
   585  	Artifacts []PerfArtifact
   586  }
   587  
   588  type PerfMetric struct {
   589  	Type string
   590  	Val  uint64
   591  }
   592  
   593  type PerfArtifact struct {
   594  	Type string
   595  	Body string
   596  }
   597  
   598  // perfResultHandler records a becnhmarking result.
   599  func perfResultHandler(r *http.Request) (interface{}, error) {
   600  	defer r.Body.Close()
   601  	if r.Method != "POST" {
   602  		return nil, errBadMethod(r.Method)
   603  	}
   604  
   605  	req := new(PerfRequest)
   606  	if err := json.NewDecoder(r.Body).Decode(req); err != nil {
   607  		return nil, fmt.Errorf("decoding Body: %v", err)
   608  	}
   609  
   610  	c := contextForRequest(r)
   611  	defer cache.Tick(c)
   612  
   613  	// store the text files if supplied
   614  	for i, a := range req.Artifacts {
   615  		hash, err := PutLog(c, a.Body)
   616  		if err != nil {
   617  			return nil, fmt.Errorf("putting Log: %v", err)
   618  		}
   619  		req.Artifacts[i].Body = hash
   620  	}
   621  	tx := func(c appengine.Context) error {
   622  		return addPerfResult(c, r, req)
   623  	}
   624  	return nil, datastore.RunInTransaction(c, tx, nil)
   625  }
   626  
   627  // addPerfResult creates PerfResult and updates Commit, PerfTodo,
   628  // PerfMetricRun and PerfConfig.
   629  // MUST be called from inside a transaction.
   630  func addPerfResult(c appengine.Context, r *http.Request, req *PerfRequest) error {
   631  	// check Package exists
   632  	p, err := GetPackage(c, "")
   633  	if err != nil {
   634  		return fmt.Errorf("GetPackage: %v", err)
   635  	}
   636  	// add result to Commit
   637  	com := &Commit{Hash: req.Hash}
   638  	if err := com.AddPerfResult(c, req.Builder, req.Benchmark); err != nil {
   639  		return fmt.Errorf("AddPerfResult: %v", err)
   640  	}
   641  
   642  	// add the result to PerfResult
   643  	res := &PerfResult{CommitHash: req.Hash}
   644  	if err := datastore.Get(c, res.Key(c), res); err != nil {
   645  		return fmt.Errorf("getting PerfResult: %v", err)
   646  	}
   647  	present := res.AddResult(req)
   648  	if _, err := datastore.Put(c, res.Key(c), res); err != nil {
   649  		return fmt.Errorf("putting PerfResult: %v", err)
   650  	}
   651  
   652  	// Meta-done denotes that there are no benchmarks left.
   653  	if req.Benchmark == "meta-done" {
   654  		// Don't send duplicate emails for the same commit/builder.
   655  		// And don't send emails about too old commits.
   656  		if !present && com.Num >= p.NextNum-commitsPerPage {
   657  			if err := checkPerfChanges(c, r, com, req.Builder, res); err != nil {
   658  				return err
   659  			}
   660  		}
   661  		if err := removeCommitFromPerfTodo(c, req.Builder, com.Num); err != nil {
   662  			return nil
   663  		}
   664  		return nil
   665  	}
   666  
   667  	// update PerfConfig
   668  	newBenchmark, err := UpdatePerfConfig(c, r, req)
   669  	if err != nil {
   670  		return fmt.Errorf("updating PerfConfig: %v", err)
   671  	}
   672  	if newBenchmark {
   673  		// If this is a new benchmark on the builder, delete PerfTodo.
   674  		// It will be recreated later with all commits again.
   675  		todo := &PerfTodo{Builder: req.Builder}
   676  		err = datastore.Delete(c, todo.Key(c))
   677  		if err != nil && err != datastore.ErrNoSuchEntity {
   678  			return fmt.Errorf("deleting PerfTodo: %v", err)
   679  		}
   680  	}
   681  
   682  	// add perf metrics
   683  	for _, metric := range req.Metrics {
   684  		m, err := GetPerfMetricRun(c, req.Builder, req.Benchmark, metric.Type, com.Num)
   685  		if err != nil {
   686  			return fmt.Errorf("GetPerfMetrics: %v", err)
   687  		}
   688  		if err = m.AddMetric(c, com.Num, metric.Val); err != nil {
   689  			return fmt.Errorf("AddMetric: %v", err)
   690  		}
   691  	}
   692  
   693  	return nil
   694  }
   695  
   696  // MUST be called from inside a transaction.
   697  func checkPerfChanges(c appengine.Context, r *http.Request, com *Commit, builder string, res *PerfResult) error {
   698  	pc, err := GetPerfConfig(c, r)
   699  	if err != nil {
   700  		return err
   701  	}
   702  
   703  	results := res.ParseData()[builder]
   704  	rcNewer := MakePerfResultCache(c, com, true)
   705  	rcOlder := MakePerfResultCache(c, com, false)
   706  
   707  	// Check whether we need to send failure notification email.
   708  	if results["meta-done"].OK {
   709  		// This one is successful, see if the next is failed.
   710  		nextRes, err := rcNewer.Next(com.Num)
   711  		if err != nil {
   712  			return err
   713  		}
   714  		if nextRes != nil && isPerfFailed(nextRes, builder) {
   715  			sendPerfFailMail(c, builder, nextRes)
   716  		}
   717  	} else {
   718  		// This one is failed, see if the previous is successful.
   719  		prevRes, err := rcOlder.Next(com.Num)
   720  		if err != nil {
   721  			return err
   722  		}
   723  		if prevRes != nil && !isPerfFailed(prevRes, builder) {
   724  			sendPerfFailMail(c, builder, res)
   725  		}
   726  	}
   727  
   728  	// Now see if there are any performance changes.
   729  	// Find the previous and the next results for performance comparison.
   730  	prevRes, err := rcOlder.NextForComparison(com.Num, builder)
   731  	if err != nil {
   732  		return err
   733  	}
   734  	nextRes, err := rcNewer.NextForComparison(com.Num, builder)
   735  	if err != nil {
   736  		return err
   737  	}
   738  	if results["meta-done"].OK {
   739  		// This one is successful, compare with a previous one.
   740  		if prevRes != nil {
   741  			if err := comparePerfResults(c, pc, builder, prevRes, res); err != nil {
   742  				return err
   743  			}
   744  		}
   745  		// Compare a next one with the current.
   746  		if nextRes != nil {
   747  			if err := comparePerfResults(c, pc, builder, res, nextRes); err != nil {
   748  				return err
   749  			}
   750  		}
   751  	} else {
   752  		// This one is failed, compare a previous one with a next one.
   753  		if prevRes != nil && nextRes != nil {
   754  			if err := comparePerfResults(c, pc, builder, prevRes, nextRes); err != nil {
   755  				return err
   756  			}
   757  		}
   758  	}
   759  
   760  	return nil
   761  }
   762  
   763  func comparePerfResults(c appengine.Context, pc *PerfConfig, builder string, prevRes, res *PerfResult) error {
   764  	changes := significantPerfChanges(pc, builder, prevRes, res)
   765  	if len(changes) == 0 {
   766  		return nil
   767  	}
   768  	com := &Commit{Hash: res.CommitHash}
   769  	if err := datastore.Get(c, com.Key(c), com); err != nil {
   770  		return fmt.Errorf("getting commit %v: %v", com.Hash, err)
   771  	}
   772  	sendPerfMailLater.Call(c, com, prevRes.CommitHash, builder, changes) // add task to queue
   773  	return nil
   774  }
   775  
   776  // logHandler displays log text for a given hash.
   777  // It handles paths like "/log/hash".
   778  func logHandler(w http.ResponseWriter, r *http.Request) {
   779  	w.Header().Set("Content-type", "text/plain; charset=utf-8")
   780  	c := contextForRequest(r)
   781  	hash := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:]
   782  	key := datastore.NewKey(c, "Log", hash, 0, nil)
   783  	l := new(Log)
   784  	if err := datastore.Get(c, key, l); err != nil {
   785  		logErr(w, r, err)
   786  		return
   787  	}
   788  	b, err := l.Text()
   789  	if err != nil {
   790  		logErr(w, r, err)
   791  		return
   792  	}
   793  	w.Write(b)
   794  }
   795  
   796  type dashHandler func(*http.Request) (interface{}, error)
   797  
   798  type dashResponse struct {
   799  	Response interface{}
   800  	Error    string
   801  }
   802  
   803  // errBadMethod is returned by a dashHandler when
   804  // the request has an unsuitable method.
   805  type errBadMethod string
   806  
   807  func (e errBadMethod) Error() string {
   808  	return "bad method: " + string(e)
   809  }
   810  
   811  // AuthHandler wraps a http.HandlerFunc with a handler that validates the
   812  // supplied key and builder query parameters.
   813  func AuthHandler(h dashHandler) http.HandlerFunc {
   814  	return func(w http.ResponseWriter, r *http.Request) {
   815  		c := contextForRequest(r)
   816  
   817  		// Put the URL Query values into r.Form to avoid parsing the
   818  		// request body when calling r.FormValue.
   819  		r.Form = r.URL.Query()
   820  
   821  		var err error
   822  		var resp interface{}
   823  
   824  		// Validate key query parameter for POST requests only.
   825  		key := r.FormValue("key")
   826  		builder := r.FormValue("builder")
   827  		if r.Method == "POST" && !validKey(c, key, builder) {
   828  			err = fmt.Errorf("invalid key %q for builder %q", key, builder)
   829  		}
   830  
   831  		// Call the original HandlerFunc and return the response.
   832  		if err == nil {
   833  			resp, err = h(r)
   834  		}
   835  
   836  		// Write JSON response.
   837  		dashResp := &dashResponse{Response: resp}
   838  		if err != nil {
   839  			c.Errorf("%v", err)
   840  			dashResp.Error = err.Error()
   841  		}
   842  		w.Header().Set("Content-Type", "application/json")
   843  		if err = json.NewEncoder(w).Encode(dashResp); err != nil {
   844  			c.Criticalf("encoding response: %v", err)
   845  		}
   846  	}
   847  }
   848  
   849  func keyHandler(w http.ResponseWriter, r *http.Request) {
   850  	builder := r.FormValue("builder")
   851  	if builder == "" {
   852  		logErr(w, r, errors.New("must supply builder in query string"))
   853  		return
   854  	}
   855  	c := contextForRequest(r)
   856  	fmt.Fprint(w, builderKey(c, builder))
   857  }
   858  
   859  func init() {
   860  	// admin handlers
   861  	handleFunc("/init", initHandler)
   862  	handleFunc("/key", keyHandler)
   863  
   864  	// authenticated handlers
   865  	handleFunc("/commit", AuthHandler(commitHandler))
   866  	handleFunc("/packages", AuthHandler(packagesHandler))
   867  	handleFunc("/result", AuthHandler(resultHandler))
   868  	handleFunc("/perf-result", AuthHandler(perfResultHandler))
   869  	handleFunc("/tag", AuthHandler(tagHandler))
   870  	handleFunc("/todo", AuthHandler(todoHandler))
   871  
   872  	// public handlers
   873  	handleFunc("/log/", logHandler)
   874  }
   875  
   876  func validHash(hash string) bool {
   877  	// TODO(adg): correctly validate a hash
   878  	return hash != ""
   879  }
   880  
   881  func validKey(c appengine.Context, key, builder string) bool {
   882  	return isMasterKey(c, key) || key == builderKey(c, builder)
   883  }
   884  
   885  func isMasterKey(c appengine.Context, k string) bool {
   886  	return appengine.IsDevAppServer() || k == key.Secret(c)
   887  }
   888  
   889  func builderKey(c appengine.Context, builder string) string {
   890  	h := hmac.New(md5.New, []byte(key.Secret(c)))
   891  	h.Write([]byte(builder))
   892  	return fmt.Sprintf("%x", h.Sum(nil))
   893  }
   894  
   895  func logErr(w http.ResponseWriter, r *http.Request, err error) {
   896  	contextForRequest(r).Errorf("Error: %v", err)
   897  	w.WriteHeader(http.StatusInternalServerError)
   898  	fmt.Fprint(w, "Error: ", err)
   899  }
   900  
   901  func contextForRequest(r *http.Request) appengine.Context {
   902  	return dashboardForRequest(r).Context(appengine.NewContext(r))
   903  }
   904  
   905  // limitStringLength essentially does return s[:max],
   906  // but it ensures that we dot not split UTF-8 rune in half.
   907  // Otherwise appengine python scripts will break badly.
   908  func limitStringLength(s string, max int) string {
   909  	if len(s) <= max {
   910  		return s
   911  	}
   912  	for {
   913  		s = s[:max]
   914  		r, size := utf8.DecodeLastRuneInString(s)
   915  		if r != utf8.RuneError || size != 1 {
   916  			return s
   917  		}
   918  		max--
   919  	}
   920  }