| // Copyright 2011 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // +build appengine |
| |
| package build |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto/hmac" |
| "crypto/md5" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "html" |
| "io/ioutil" |
| "net/http" |
| "strconv" |
| "strings" |
| "time" |
| "unicode/utf8" |
| |
| "golang.org/x/build/app/cache" |
| "golang.org/x/build/app/key" |
| "google.golang.org/appengine" |
| "google.golang.org/appengine/datastore" |
| "google.golang.org/appengine/log" |
| "google.golang.org/appengine/memcache" |
| ) |
| |
| const ( |
| commitsPerPage = 30 |
| watcherVersion = 3 // must match dashboard/watcher/watcher.go |
| builderVersion = 1 // must match dashboard/builder/http.go |
| ) |
| |
| // commitHandler retrieves commit data or records a new commit. |
| // |
| // For GET requests it returns a Commit value for the specified |
| // packagePath and hash. |
| // |
| // For POST requests it reads a JSON-encoded Commit value from the request |
| // body and creates a new Commit entity. It also updates the "tip" Tag for |
| // each new commit at tip. |
| // |
| // This handler is used by a gobuilder process in -commit mode. |
| func commitHandler(r *http.Request) (interface{}, error) { |
| c := contextForRequest(r) |
| com := new(Commit) |
| |
| if r.Method == "GET" { |
| com.PackagePath = r.FormValue("packagePath") |
| com.Hash = r.FormValue("hash") |
| err := datastore.Get(c, com.Key(c), com) |
| if com.Num == 0 && com.Desc == "" { |
| // Perf builder might have written an incomplete Commit. |
| // Pretend it doesn't exist, so that we can get complete details. |
| err = datastore.ErrNoSuchEntity |
| } |
| if err != nil { |
| if err == datastore.ErrNoSuchEntity { |
| // This error string is special. |
| // The commit watcher expects it. |
| // Do not change it. |
| return nil, errors.New("Commit not found") |
| } |
| return nil, fmt.Errorf("getting Commit: %v", err) |
| } |
| if com.Num == 0 { |
| // Corrupt state which shouldn't happen but does. |
| // Return an error so builders' commit loops will |
| // be willing to retry submitting this commit. |
| return nil, errors.New("in datastore with zero Num") |
| } |
| if com.Desc == "" || com.User == "" { |
| // Also shouldn't happen, but at least happened |
| // once on a single commit when trying to fix data |
| // in the datastore viewer UI? |
| return nil, errors.New("missing field") |
| } |
| // Strip potentially large and unnecessary fields. |
| com.ResultData = nil |
| com.PerfResults = nil |
| return com, nil |
| } |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| if !isMasterKey(c, r.FormValue("key")) { |
| return nil, errors.New("can only POST commits with master key") |
| } |
| |
| // For now, the commit watcher doesn't support gccgo. |
| // TODO(adg,cmang): remove this exception when gccgo is supported. |
| if dashboardForRequest(r) != gccgoDash { |
| v, _ := strconv.Atoi(r.FormValue("version")) |
| if v != watcherVersion { |
| return nil, fmt.Errorf("rejecting POST from commit watcher; need version %v instead of %v", |
| watcherVersion, v) |
| } |
| } |
| |
| // POST request |
| body, err := ioutil.ReadAll(r.Body) |
| r.Body.Close() |
| if err != nil { |
| return nil, fmt.Errorf("reading Body: %v", err) |
| } |
| if !bytes.Contains(body, needsBenchmarkingBytes) { |
| log.Warningf(c, "old builder detected at %v", r.RemoteAddr) |
| return nil, fmt.Errorf("rejecting old builder request, body does not contain %s: %q", needsBenchmarkingBytes, body) |
| } |
| if err := json.Unmarshal(body, com); err != nil { |
| return nil, fmt.Errorf("unmarshaling body %q: %v", body, err) |
| } |
| com.Desc = limitStringLength(com.Desc, maxDatastoreStringLen) |
| if err := com.Valid(); err != nil { |
| return nil, fmt.Errorf("validating Commit: %v", err) |
| } |
| defer cache.Tick(c) |
| tx := func(c context.Context) error { |
| return addCommit(c, com) |
| } |
| return nil, datastore.RunInTransaction(c, tx, nil) |
| } |
| |
| var needsBenchmarkingBytes = []byte(`"NeedsBenchmarking"`) |
| |
| // addCommit adds the Commit entity to the datastore and updates the tip Tag. |
| // It must be run inside a datastore transaction. |
| func addCommit(c context.Context, com *Commit) error { |
| var ec Commit // existing commit |
| isUpdate := false |
| err := datastore.Get(c, com.Key(c), &ec) |
| if err != nil && err != datastore.ErrNoSuchEntity { |
| return fmt.Errorf("getting Commit: %v", err) |
| } |
| if err == nil { |
| // Commit already in the datastore. Any fields different? |
| // If not, don't do anything. |
| changes := (com.Num != 0 && com.Num != ec.Num) || |
| com.ParentHash != ec.ParentHash || |
| com.Desc != ec.Desc || |
| com.User != ec.User || |
| !com.Time.Equal(ec.Time) |
| if !changes { |
| return nil |
| } |
| ec.ParentHash = com.ParentHash |
| ec.Desc = com.Desc |
| ec.User = com.User |
| if !com.Time.IsZero() { |
| ec.Time = com.Time |
| } |
| if com.Num != 0 { |
| ec.Num = com.Num |
| } |
| isUpdate = true |
| com = &ec |
| } |
| p, err := GetPackage(c, com.PackagePath) |
| if err != nil { |
| return fmt.Errorf("GetPackage: %v", err) |
| } |
| if com.Num == 0 { |
| // get the next commit number |
| com.Num = p.NextNum |
| p.NextNum++ |
| if _, err := datastore.Put(c, p.Key(c), p); err != nil { |
| return fmt.Errorf("putting Package: %v", err) |
| } |
| } else if com.Num >= p.NextNum { |
| p.NextNum = com.Num + 1 |
| if _, err := datastore.Put(c, p.Key(c), p); err != nil { |
| return fmt.Errorf("putting Package: %v", err) |
| } |
| } |
| // if this isn't the first Commit test the parent commit exists. |
| // The all zeros are returned by hg's p1node template for parentless commits. |
| if com.ParentHash != "" && com.ParentHash != "0000000000000000000000000000000000000000" && com.ParentHash != "0000" { |
| n, err := datastore.NewQuery("Commit"). |
| Filter("Hash =", com.ParentHash). |
| Ancestor(p.Key(c)). |
| Count(c) |
| if err != nil { |
| return fmt.Errorf("testing for parent Commit: %v", err) |
| } |
| if n == 0 { |
| return errors.New("parent commit not found") |
| } |
| } else if com.Num != 1 { |
| // This is the first commit; fail if it is not number 1. |
| // (This will happen if we try to upload a new/different repo |
| // where there is already commit data. A bad thing to do.) |
| return errors.New("this package already has a first commit; aborting") |
| } |
| // Update the relevant Tag entity, if applicable. |
| if !isUpdate && p.Path == "" { |
| var t *Tag |
| if com.Branch == "master" { |
| t = &Tag{Kind: "tip", Hash: com.Hash} |
| } |
| if strings.HasPrefix(com.Branch, "release-branch.") { |
| t = &Tag{Kind: "release", Name: com.Branch, Hash: com.Hash} |
| } |
| if t != nil { |
| if _, err = datastore.Put(c, t.Key(c), t); err != nil { |
| return fmt.Errorf("putting Tag: %v", err) |
| } |
| } |
| } |
| // put the Commit |
| if err = putCommit(c, com); err != nil { |
| return err |
| } |
| if com.NeedsBenchmarking { |
| // add to CommitRun |
| cr, err := GetCommitRun(c, com.Num) |
| if err != nil { |
| return err |
| } |
| if err = cr.AddCommit(c, com); err != nil { |
| return err |
| } |
| // create PerfResult |
| res := &PerfResult{CommitHash: com.Hash, CommitNum: com.Num} |
| if _, err := datastore.Put(c, res.Key(c), res); err != nil { |
| return fmt.Errorf("putting PerfResult: %v", err) |
| } |
| // Update perf todo if necessary. |
| if err = AddCommitToPerfTodo(c, com); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| // tagHandler records a new tag. It reads a JSON-encoded Tag value from the |
| // request body and updates the Tag entity for the Kind of tag provided. |
| // |
| // This handler is used by a gobuilder process in -commit mode. |
| func tagHandler(r *http.Request) (interface{}, error) { |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| |
| t := new(Tag) |
| defer r.Body.Close() |
| if err := json.NewDecoder(r.Body).Decode(t); err != nil { |
| return nil, err |
| } |
| if err := t.Valid(); err != nil { |
| return nil, err |
| } |
| c := contextForRequest(r) |
| defer cache.Tick(c) |
| _, err := datastore.Put(c, t.Key(c), t) |
| return nil, err |
| } |
| |
| // Todo is a todoHandler response. |
| type Todo struct { |
| Kind string // "build-go-commit" or "build-package" |
| Data interface{} |
| } |
| |
| // todoHandler returns the next action to be performed by a builder. |
| // It expects "builder" and "kind" query parameters and returns a *Todo value. |
| // Multiple "kind" parameters may be specified. |
| func todoHandler(r *http.Request) (interface{}, error) { |
| c := contextForRequest(r) |
| now := cache.Now(c) |
| key := "build-todo-" + r.Form.Encode() |
| var todo *Todo |
| if cache.Get(c, r, now, key, &todo) { |
| // Hack to avoid storing nil in memcache. |
| if todo.Kind == "none" { |
| return nil, nil |
| } |
| return todo, nil |
| } |
| var err error |
| builder := r.FormValue("builder") |
| if builderKeyRevoked(builder) { |
| return nil, fmt.Errorf("builder key revoked; no work given") |
| } |
| for _, kind := range r.Form["kind"] { |
| var com *Commit |
| switch kind { |
| case "build-go-commit": |
| com, err = buildTodo(c, builder, "", "") |
| if com != nil { |
| com.PerfResults = []string{} |
| } |
| case "build-package": |
| packagePath := r.FormValue("packagePath") |
| goHash := r.FormValue("goHash") |
| com, err = buildTodo(c, builder, packagePath, goHash) |
| if com != nil { |
| com.PerfResults = []string{} |
| } |
| case "benchmark-go-commit": |
| com, err = perfTodo(c, builder) |
| } |
| if com != nil || err != nil { |
| if com != nil { |
| // ResultData can be large and not needed on builder. |
| com.ResultData = []string{} |
| } |
| todo = &Todo{Kind: kind, Data: com} |
| break |
| } |
| } |
| if err == nil { |
| // Hack to avoid storing nil in memcache. |
| if todo == nil { |
| todo = &Todo{Kind: "none"} |
| } |
| cache.Set(c, r, now, key, todo) |
| } |
| // Hack to avoid storing nil in memcache. |
| if todo.Kind == "none" { |
| return nil, nil |
| } |
| return todo, err |
| } |
| |
| // buildTodo returns the next Commit to be built (or nil if none available). |
| // |
| // If packagePath and goHash are empty, it scans the first 20 Go Commits in |
| // Num-descending order and returns the first one it finds that doesn't have a |
| // Result for this builder. |
| // |
| // If provided with non-empty packagePath and goHash args, it scans the first |
| // 20 Commits in Num-descending order for the specified packagePath and |
| // returns the first that doesn't have a Result for this builder and goHash. |
| func buildTodo(c context.Context, builder, packagePath, goHash string) (*Commit, error) { |
| p, err := GetPackage(c, packagePath) |
| if err != nil { |
| return nil, err |
| } |
| |
| t := datastore.NewQuery("Commit"). |
| Ancestor(p.Key(c)). |
| Limit(commitsPerPage). |
| Order("-Num"). |
| Run(c) |
| for { |
| com := new(Commit) |
| if _, err := t.Next(com); err == datastore.Done { |
| break |
| } else if err != nil { |
| return nil, err |
| } |
| if com.Result(builder, goHash) == nil { |
| return com, nil |
| } |
| } |
| |
| // Nothing left to do if this is a package (not the Go tree). |
| if packagePath != "" { |
| return nil, nil |
| } |
| |
| // If there are no Go tree commits left to build, |
| // see if there are any subrepo commits that need to be built at tip. |
| // If so, ask the builder to build a go tree at the tip commit. |
| // TODO(adg): do the same for "weekly" and "release" tags. |
| |
| tag, err := GetTag(c, "tip", "") |
| if err != nil { |
| return nil, err |
| } |
| |
| // Check that this Go commit builds OK for this builder. |
| // If not, don't re-build as the subrepos will never get built anyway. |
| com, err := tag.Commit(c) |
| if err != nil { |
| return nil, err |
| } |
| if r := com.Result(builder, ""); r != nil && !r.OK { |
| return nil, nil |
| } |
| |
| pkgs, err := Packages(c, "subrepo") |
| if err != nil { |
| return nil, err |
| } |
| for _, pkg := range pkgs { |
| com, err := pkg.LastCommit(c) |
| if err != nil { |
| log.Warningf(c, "%v: no Commit found: %v", pkg, err) |
| continue |
| } |
| if com.Result(builder, tag.Hash) == nil { |
| return tag.Commit(c) |
| } |
| } |
| |
| return nil, nil |
| } |
| |
| // perfTodo returns the next Commit to be benchmarked (or nil if none available). |
| func perfTodo(c context.Context, builder string) (*Commit, error) { |
| p := &Package{} |
| todo := &PerfTodo{Builder: builder} |
| err := datastore.Get(c, todo.Key(c), todo) |
| if err != nil && err != datastore.ErrNoSuchEntity { |
| return nil, fmt.Errorf("fetching PerfTodo: %v", err) |
| } |
| if err == datastore.ErrNoSuchEntity { |
| todo, err = buildPerfTodo(c, builder) |
| if err != nil { |
| return nil, err |
| } |
| } |
| if len(todo.CommitNums) == 0 { |
| return nil, nil |
| } |
| |
| // Have commit to benchmark, fetch it. |
| num := todo.CommitNums[len(todo.CommitNums)-1] |
| t := datastore.NewQuery("Commit"). |
| Ancestor(p.Key(c)). |
| Filter("Num =", num). |
| Limit(1). |
| Run(c) |
| com := new(Commit) |
| if _, err := t.Next(com); err != nil { |
| return nil, err |
| } |
| if !com.NeedsBenchmarking { |
| return nil, fmt.Errorf("commit from perf todo queue is not intended for benchmarking") |
| } |
| |
| // Remove benchmarks from other builders. |
| var benchs []string |
| for _, b := range com.PerfResults { |
| bb := strings.Split(b, "|") |
| if bb[0] == builder && bb[1] != "meta-done" { |
| benchs = append(benchs, bb[1]) |
| } |
| } |
| com.PerfResults = benchs |
| |
| return com, nil |
| } |
| |
| // buildPerfTodo creates PerfTodo for the builder with all commits. In a transaction. |
| func buildPerfTodo(c context.Context, builder string) (*PerfTodo, error) { |
| todo := &PerfTodo{Builder: builder} |
| tx := func(c context.Context) error { |
| err := datastore.Get(c, todo.Key(c), todo) |
| if err != nil && err != datastore.ErrNoSuchEntity { |
| return fmt.Errorf("fetching PerfTodo: %v", err) |
| } |
| if err == nil { |
| return nil |
| } |
| t := datastore.NewQuery("CommitRun"). |
| Ancestor((&Package{}).Key(c)). |
| Order("-StartCommitNum"). |
| Run(c) |
| var nums []int |
| var releaseNums []int |
| loop: |
| for { |
| cr := new(CommitRun) |
| if _, err := t.Next(cr); err == datastore.Done { |
| break |
| } else if err != nil { |
| return fmt.Errorf("scanning commit runs for perf todo: %v", err) |
| } |
| for i := len(cr.Hash) - 1; i >= 0; i-- { |
| if !cr.NeedsBenchmarking[i] || cr.Hash[i] == "" { |
| continue // There's nothing to see here. Move along. |
| } |
| num := cr.StartCommitNum + i |
| for k, v := range knownTags { |
| // Releases are benchmarked first, because they are important (and there are few of them). |
| if cr.Hash[i] == v { |
| releaseNums = append(releaseNums, num) |
| if k == "go1" { |
| break loop // Point of no benchmark: test/bench/shootout: update timing.log to Go 1. |
| } |
| } |
| } |
| nums = append(nums, num) |
| } |
| } |
| todo.CommitNums = orderPerfTodo(nums) |
| todo.CommitNums = append(todo.CommitNums, releaseNums...) |
| if _, err = datastore.Put(c, todo.Key(c), todo); err != nil { |
| return fmt.Errorf("putting PerfTodo: %v", err) |
| } |
| return nil |
| } |
| return todo, datastore.RunInTransaction(c, tx, nil) |
| } |
| |
| func removeCommitFromPerfTodo(c context.Context, builder string, num int) error { |
| todo := &PerfTodo{Builder: builder} |
| err := datastore.Get(c, todo.Key(c), todo) |
| if err != nil && err != datastore.ErrNoSuchEntity { |
| return fmt.Errorf("fetching PerfTodo: %v", err) |
| } |
| if err == datastore.ErrNoSuchEntity { |
| return nil |
| } |
| for i := len(todo.CommitNums) - 1; i >= 0; i-- { |
| if todo.CommitNums[i] == num { |
| for ; i < len(todo.CommitNums)-1; i++ { |
| todo.CommitNums[i] = todo.CommitNums[i+1] |
| } |
| todo.CommitNums = todo.CommitNums[:i] |
| _, err = datastore.Put(c, todo.Key(c), todo) |
| if err != nil { |
| return fmt.Errorf("putting PerfTodo: %v", err) |
| } |
| break |
| } |
| } |
| return nil |
| } |
| |
| // packagesHandler returns a list of the non-Go Packages monitored |
| // by the dashboard. |
| func packagesHandler(r *http.Request) (interface{}, error) { |
| kind := r.FormValue("kind") |
| c := contextForRequest(r) |
| now := cache.Now(c) |
| key := "build-packages-" + kind |
| var p []*Package |
| if cache.Get(c, r, now, key, &p) { |
| return p, nil |
| } |
| p, err := Packages(c, kind) |
| if err != nil { |
| return nil, err |
| } |
| cache.Set(c, r, now, key, p) |
| return p, nil |
| } |
| |
| // buildingHandler records that a build is in progress. |
| // The data is only stored in memcache and with a timeout. It's assumed |
| // that the build system will periodically refresh this if the build |
| // is slow. |
| func buildingHandler(r *http.Request) (interface{}, error) { |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| c := contextForRequest(r) |
| key := buildingKey(r.FormValue("hash"), r.FormValue("gohash"), r.FormValue("builder")) |
| err := memcache.Set(c, &memcache.Item{ |
| Key: key, |
| Value: []byte(r.FormValue("url")), |
| Expiration: 15 * time.Minute, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| return map[string]interface{}{ |
| "key": key, |
| }, nil |
| } |
| |
| // resultHandler records a build result. |
| // It reads a JSON-encoded Result value from the request body, |
| // creates a new Result entity, and updates the relevant Commit entity. |
| // If the Log field is not empty, resultHandler creates a new Log entity |
| // and updates the LogHash field before putting the Commit entity. |
| func resultHandler(r *http.Request) (interface{}, error) { |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| |
| // For now, the gccgo builders are using the old stuff. |
| // TODO(adg,cmang): remove this exception when gccgo is updated. |
| if dashboardForRequest(r) != gccgoDash { |
| v, _ := strconv.Atoi(r.FormValue("version")) |
| if v != builderVersion { |
| return nil, fmt.Errorf("rejecting POST from builder; need version %v instead of %v", |
| builderVersion, v) |
| } |
| } |
| |
| c := contextForRequest(r) |
| res := new(Result) |
| defer r.Body.Close() |
| if err := json.NewDecoder(r.Body).Decode(res); err != nil { |
| return nil, fmt.Errorf("decoding Body: %v", err) |
| } |
| if err := res.Valid(); err != nil { |
| return nil, fmt.Errorf("validating Result: %v", err) |
| } |
| defer cache.Tick(c) |
| // store the Log text if supplied |
| if len(res.Log) > 0 { |
| hash, err := PutLog(c, res.Log) |
| if err != nil { |
| return nil, fmt.Errorf("putting Log: %v", err) |
| } |
| res.LogHash = hash |
| } |
| tx := func(c context.Context) error { |
| // check Package exists |
| if _, err := GetPackage(c, res.PackagePath); err != nil { |
| return fmt.Errorf("GetPackage: %v", err) |
| } |
| // put Result |
| if _, err := datastore.Put(c, res.Key(c), res); err != nil { |
| return fmt.Errorf("putting Result: %v", err) |
| } |
| // add Result to Commit |
| com := &Commit{PackagePath: res.PackagePath, Hash: res.Hash} |
| if err := com.AddResult(c, res); err != nil { |
| return fmt.Errorf("AddResult: %v", err) |
| } |
| // Send build failure notifications, if necessary. |
| // Note this must run after the call AddResult, which |
| // populates the Commit's ResultData field. |
| return notifyOnFailure(c, com, res.Builder) |
| } |
| return nil, datastore.RunInTransaction(c, tx, nil) |
| } |
| |
| // perf-result request payload |
| type PerfRequest struct { |
| Builder string |
| Benchmark string |
| Hash string |
| OK bool |
| Metrics []PerfMetric |
| Artifacts []PerfArtifact |
| } |
| |
| type PerfMetric struct { |
| Type string |
| Val uint64 |
| } |
| |
| type PerfArtifact struct { |
| Type string |
| Body string |
| } |
| |
| // perfResultHandler records a becnhmarking result. |
| func perfResultHandler(r *http.Request) (interface{}, error) { |
| defer r.Body.Close() |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| |
| req := new(PerfRequest) |
| if err := json.NewDecoder(r.Body).Decode(req); err != nil { |
| return nil, fmt.Errorf("decoding Body: %v", err) |
| } |
| |
| c := contextForRequest(r) |
| defer cache.Tick(c) |
| |
| // store the text files if supplied |
| for i, a := range req.Artifacts { |
| hash, err := PutLog(c, a.Body) |
| if err != nil { |
| return nil, fmt.Errorf("putting Log: %v", err) |
| } |
| req.Artifacts[i].Body = hash |
| } |
| tx := func(c context.Context) error { |
| return addPerfResult(c, r, req) |
| } |
| return nil, datastore.RunInTransaction(c, tx, nil) |
| } |
| |
| // addPerfResult creates PerfResult and updates Commit, PerfTodo, |
| // PerfMetricRun and PerfConfig. |
| // MUST be called from inside a transaction. |
| func addPerfResult(c context.Context, r *http.Request, req *PerfRequest) error { |
| // check Package exists |
| p, err := GetPackage(c, "") |
| if err != nil { |
| return fmt.Errorf("GetPackage: %v", err) |
| } |
| // add result to Commit |
| com := &Commit{Hash: req.Hash} |
| if err := com.AddPerfResult(c, req.Builder, req.Benchmark); err != nil { |
| return fmt.Errorf("AddPerfResult: %v", err) |
| } |
| |
| // add the result to PerfResult |
| res := &PerfResult{CommitHash: req.Hash} |
| if err := datastore.Get(c, res.Key(c), res); err != nil { |
| return fmt.Errorf("getting PerfResult: %v", err) |
| } |
| present := res.AddResult(req) |
| if _, err := datastore.Put(c, res.Key(c), res); err != nil { |
| return fmt.Errorf("putting PerfResult: %v", err) |
| } |
| |
| // Meta-done denotes that there are no benchmarks left. |
| if req.Benchmark == "meta-done" { |
| // Don't send duplicate emails for the same commit/builder. |
| // And don't send emails about too old commits. |
| if !present && com.Num >= p.NextNum-commitsPerPage { |
| if err := checkPerfChanges(c, r, com, req.Builder, res); err != nil { |
| return err |
| } |
| } |
| if err := removeCommitFromPerfTodo(c, req.Builder, com.Num); err != nil { |
| return nil |
| } |
| return nil |
| } |
| |
| // update PerfConfig |
| newBenchmark, err := UpdatePerfConfig(c, r, req) |
| if err != nil { |
| return fmt.Errorf("updating PerfConfig: %v", err) |
| } |
| if newBenchmark { |
| // If this is a new benchmark on the builder, delete PerfTodo. |
| // It will be recreated later with all commits again. |
| todo := &PerfTodo{Builder: req.Builder} |
| err = datastore.Delete(c, todo.Key(c)) |
| if err != nil && err != datastore.ErrNoSuchEntity { |
| return fmt.Errorf("deleting PerfTodo: %v", err) |
| } |
| } |
| |
| // add perf metrics |
| for _, metric := range req.Metrics { |
| m, err := GetPerfMetricRun(c, req.Builder, req.Benchmark, metric.Type, com.Num) |
| if err != nil { |
| return fmt.Errorf("GetPerfMetrics: %v", err) |
| } |
| if err = m.AddMetric(c, com.Num, metric.Val); err != nil { |
| return fmt.Errorf("AddMetric: %v", err) |
| } |
| } |
| |
| return nil |
| } |
| |
| // MUST be called from inside a transaction. |
| func checkPerfChanges(c context.Context, r *http.Request, com *Commit, builder string, res *PerfResult) error { |
| pc, err := GetPerfConfig(c, r) |
| if err != nil { |
| return err |
| } |
| |
| results := res.ParseData()[builder] |
| rcNewer := MakePerfResultCache(c, com, true) |
| rcOlder := MakePerfResultCache(c, com, false) |
| |
| // Check whether we need to send failure notification email. |
| if results["meta-done"].OK { |
| // This one is successful, see if the next is failed. |
| nextRes, err := rcNewer.Next(com.Num) |
| if err != nil { |
| return err |
| } |
| if nextRes != nil && isPerfFailed(nextRes, builder) { |
| sendPerfFailMail(c, builder, nextRes) |
| } |
| } else { |
| // This one is failed, see if the previous is successful. |
| prevRes, err := rcOlder.Next(com.Num) |
| if err != nil { |
| return err |
| } |
| if prevRes != nil && !isPerfFailed(prevRes, builder) { |
| sendPerfFailMail(c, builder, res) |
| } |
| } |
| |
| // Now see if there are any performance changes. |
| // Find the previous and the next results for performance comparison. |
| prevRes, err := rcOlder.NextForComparison(com.Num, builder) |
| if err != nil { |
| return err |
| } |
| nextRes, err := rcNewer.NextForComparison(com.Num, builder) |
| if err != nil { |
| return err |
| } |
| if results["meta-done"].OK { |
| // This one is successful, compare with a previous one. |
| if prevRes != nil { |
| if err := comparePerfResults(c, pc, builder, prevRes, res); err != nil { |
| return err |
| } |
| } |
| // Compare a next one with the current. |
| if nextRes != nil { |
| if err := comparePerfResults(c, pc, builder, res, nextRes); err != nil { |
| return err |
| } |
| } |
| } else { |
| // This one is failed, compare a previous one with a next one. |
| if prevRes != nil && nextRes != nil { |
| if err := comparePerfResults(c, pc, builder, prevRes, nextRes); err != nil { |
| return err |
| } |
| } |
| } |
| |
| return nil |
| } |
| |
| func comparePerfResults(c context.Context, pc *PerfConfig, builder string, prevRes, res *PerfResult) error { |
| changes := significantPerfChanges(pc, builder, prevRes, res) |
| if len(changes) == 0 { |
| return nil |
| } |
| com := &Commit{Hash: res.CommitHash} |
| if err := datastore.Get(c, com.Key(c), com); err != nil { |
| return fmt.Errorf("getting commit %v: %v", com.Hash, err) |
| } |
| sendPerfMailLater.Call(c, com, prevRes.CommitHash, builder, changes) // add task to queue |
| return nil |
| } |
| |
| // logHandler displays log text for a given hash. |
| // It handles paths like "/log/hash". |
| func logHandler(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-type", "text/plain; charset=utf-8") |
| c := contextForRequest(r) |
| hash := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] |
| key := datastore.NewKey(c, "Log", hash, 0, nil) |
| l := new(Log) |
| if err := datastore.Get(c, key, l); err != nil { |
| if err == datastore.ErrNoSuchEntity { |
| // Fall back to default namespace; |
| // maybe this was on the old dashboard. |
| c := appengine.NewContext(r) |
| key := datastore.NewKey(c, "Log", hash, 0, nil) |
| err = datastore.Get(c, key, l) |
| } |
| if err != nil { |
| logErr(w, r, err) |
| return |
| } |
| } |
| b, err := l.Text() |
| if err != nil { |
| logErr(w, r, err) |
| return |
| } |
| w.Write(b) |
| } |
| |
| // clearResultsHandler purges the last commitsPerPage results for the given builder. |
| // It optionally takes a comma-separated list of specific hashes to clear. |
| func clearResultsHandler(r *http.Request) (interface{}, error) { |
| if r.Method != "POST" { |
| return nil, errBadMethod(r.Method) |
| } |
| builder := r.FormValue("builder") |
| if builder == "" { |
| return nil, errors.New("must specify a builder") |
| } |
| clearAll := r.FormValue("hash") == "" |
| hash := strings.Split(r.FormValue("hash"), ",") |
| |
| c := contextForRequest(r) |
| defer cache.Tick(c) |
| pkg := (&Package{}).Key(c) // TODO(adg): support clearing sub-repos |
| err := datastore.RunInTransaction(c, func(c context.Context) error { |
| var coms []*Commit |
| keys, err := datastore.NewQuery("Commit"). |
| Ancestor(pkg). |
| Order("-Num"). |
| Limit(commitsPerPage). |
| GetAll(c, &coms) |
| if err != nil { |
| return err |
| } |
| var rKeys []*datastore.Key |
| for _, com := range coms { |
| if !(clearAll || contains(hash, com.Hash)) { |
| continue |
| } |
| r := com.Result(builder, "") |
| if r == nil { |
| continue |
| } |
| com.RemoveResult(r) |
| rKeys = append(rKeys, r.Key(c)) |
| } |
| _, err = datastore.PutMulti(c, keys, coms) |
| if err != nil { |
| return err |
| } |
| return datastore.DeleteMulti(c, rKeys) |
| }, nil) |
| return nil, err |
| } |
| |
| type dashHandler func(*http.Request) (interface{}, error) |
| |
| type dashResponse struct { |
| Response interface{} |
| Error string |
| } |
| |
| // errBadMethod is returned by a dashHandler when |
| // the request has an unsuitable method. |
| type errBadMethod string |
| |
| func (e errBadMethod) Error() string { |
| return "bad method: " + string(e) |
| } |
| |
| func builderKeyRevoked(builder string) bool { |
| switch builder { |
| case "plan9-amd64-mischief": |
| // Broken and unmaintained for months. |
| // It's polluting the dashboard. |
| return true |
| case "linux-arm-onlinenet": |
| // Requested to be revoked by Dave Cheney. |
| // The machine is in a fail+report loop |
| // and can't be accessed. Revoke it for now. |
| return true |
| } |
| return false |
| } |
| |
| // AuthHandler wraps a http.HandlerFunc with a handler that validates the |
| // supplied key and builder query parameters. |
| func AuthHandler(h dashHandler) http.HandlerFunc { |
| return func(w http.ResponseWriter, r *http.Request) { |
| c := contextForRequest(r) |
| |
| // Put the URL Query values into r.Form to avoid parsing the |
| // request body when calling r.FormValue. |
| r.Form = r.URL.Query() |
| |
| var err error |
| var resp interface{} |
| |
| // Validate key query parameter for POST requests only. |
| key := r.FormValue("key") |
| builder := r.FormValue("builder") |
| if r.Method == "POST" && !validKey(c, key, builder) { |
| err = fmt.Errorf("invalid key %q for builder %q", key, builder) |
| } |
| |
| // Call the original HandlerFunc and return the response. |
| if err == nil { |
| resp, err = h(r) |
| } |
| |
| // Write JSON response. |
| dashResp := &dashResponse{Response: resp} |
| if err != nil { |
| log.Errorf(c, "%v", err) |
| dashResp.Error = err.Error() |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if err = json.NewEncoder(w).Encode(dashResp); err != nil { |
| log.Criticalf(c, "encoding response: %v", err) |
| } |
| } |
| } |
| |
| func keyHandler(w http.ResponseWriter, r *http.Request) { |
| builder := r.FormValue("builder") |
| if builder == "" { |
| logErr(w, r, errors.New("must supply builder in query string")) |
| return |
| } |
| c := contextForRequest(r) |
| fmt.Fprint(w, builderKey(c, builder)) |
| } |
| |
| func init() { |
| // admin handlers |
| handleFunc("/init", initHandler) |
| handleFunc("/key", keyHandler) |
| |
| // authenticated handlers |
| handleFunc("/building", AuthHandler(buildingHandler)) |
| handleFunc("/clear-results", AuthHandler(clearResultsHandler)) |
| handleFunc("/commit", AuthHandler(commitHandler)) |
| handleFunc("/packages", AuthHandler(packagesHandler)) |
| handleFunc("/perf-result", AuthHandler(perfResultHandler)) |
| handleFunc("/result", AuthHandler(resultHandler)) |
| handleFunc("/tag", AuthHandler(tagHandler)) |
| handleFunc("/todo", AuthHandler(todoHandler)) |
| |
| // public handlers |
| handleFunc("/log/", logHandler) |
| } |
| |
| func validHash(hash string) bool { |
| // TODO(adg): correctly validate a hash |
| return hash != "" |
| } |
| |
| func validKey(c context.Context, key, builder string) bool { |
| if isMasterKey(c, key) { |
| return true |
| } |
| if builderKeyRevoked(builder) { |
| return false |
| } |
| return key == builderKey(c, builder) |
| } |
| |
| func isMasterKey(c context.Context, k string) bool { |
| return appengine.IsDevAppServer() || k == key.Secret(c) |
| } |
| |
| func builderKey(c context.Context, builder string) string { |
| h := hmac.New(md5.New, []byte(key.Secret(c))) |
| h.Write([]byte(builder)) |
| return fmt.Sprintf("%x", h.Sum(nil)) |
| } |
| |
| func logErr(w http.ResponseWriter, r *http.Request, err error) { |
| c := contextForRequest(r) |
| log.Errorf(c, "Error: %v", err) |
| w.WriteHeader(http.StatusInternalServerError) |
| fmt.Fprint(w, "Error: ", html.EscapeString(err.Error())) |
| } |
| |
| func contextForRequest(r *http.Request) context.Context { |
| return dashboardForRequest(r).Context(appengine.NewContext(r)) |
| } |
| |
| // limitStringLength essentially does return s[:max], |
| // but it ensures that we dot not split UTF-8 rune in half. |
| // Otherwise appengine python scripts will break badly. |
| func limitStringLength(s string, max int) string { |
| if len(s) <= max { |
| return s |
| } |
| for { |
| s = s[:max] |
| r, size := utf8.DecodeLastRuneInString(s) |
| if r != utf8.RuneError || size != 1 { |
| return s |
| } |
| max-- |
| } |
| } |