2014-12-14 20:12:14 -07:00
// Copyright 2014 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.
// The retrybuilds command clears build failures from the build.golang.org dashboard
// to force them to be rebuilt.
//
// Valid usage modes:
//
// retrybuilds -loghash=f45f0eb8
// retrybuilds -builder=openbsd-amd64
// retrybuilds -builder=openbsd-amd64 -hash=6fecb7
// retrybuilds -redo-flaky
// retrybuilds -redo-flaky -builder=linux-amd64-clang
package main
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
)
var (
masterKeyFile = flag . String ( "masterkey" , filepath . Join ( os . Getenv ( "HOME" ) , "keys" , "gobuilder-master.key" ) , "path to Go builder master key. If present, the key argument is not necessary" )
keyFile = flag . String ( "key" , "" , "path to key file" )
builder = flag . String ( "builder" , "" , "builder to wipe a result for." )
hash = flag . String ( "hash" , "" , "Hash to wipe. If empty, all will be wiped." )
redoFlaky = flag . Bool ( "redo-flaky" , false , "Reset all flaky builds. If builder is empty, the master key is required." )
builderPrefix = flag . String ( "builder-prefix" , "https://build.golang.org" , "builder URL prefix" )
logHash = flag . String ( "loghash" , "" , "If non-empty, clear the build that failed with this loghash prefix" )
)
type Failure struct {
Builder string
Hash string
LogURL string
}
func main ( ) {
flag . Parse ( )
* builderPrefix = strings . TrimSuffix ( * builderPrefix , "/" )
if * logHash != "" {
substr := "/log/" + * logHash
for _ , f := range failures ( ) {
if strings . Contains ( f . LogURL , substr ) {
wipe ( f . Builder , f . Hash )
}
}
return
}
if * redoFlaky {
fixTheFlakes ( )
return
}
if * builder == "" {
log . Fatalf ( "Missing -builder, -redo-flaky, or -loghash flag." )
}
wipe ( * builder , fullHash ( * hash ) )
}
func fixTheFlakes ( ) {
gate := make ( chan bool , 50 )
var wg sync . WaitGroup
for _ , f := range failures ( ) {
f := f
if * builder != "" && f . Builder != * builder {
continue
}
gate <- true
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
defer func ( ) { <- gate } ( )
res , err := http . Get ( f . LogURL )
if err != nil {
log . Fatalf ( "Error fetching %s: %v" , f . LogURL , err )
}
defer res . Body . Close ( )
failLog , err := ioutil . ReadAll ( res . Body )
if err != nil {
log . Fatalf ( "Error reading %s: %v" , f . LogURL , err )
}
if isFlaky ( string ( failLog ) ) {
log . Printf ( "Restarting flaky %+v" , f )
wipe ( f . Builder , f . Hash )
}
} ( )
}
wg . Wait ( )
}
var flakePhrases = [ ] string {
"No space left on device" ,
"fatal error: error in backend: IO failure on output stream" ,
"Boffset: unknown state 0" ,
"Bseek: unknown state 0" ,
"error exporting repository: exit status" ,
"remote error: User Is Over Quota" ,
2014-12-19 23:07:06 -07:00
"fatal: remote did not send all necessary objects" ,
2014-12-14 20:12:14 -07:00
}
func isFlaky ( failLog string ) bool {
if strings . HasPrefix ( failLog , "exit status " ) {
return true
}
for _ , phrase := range flakePhrases {
if strings . Contains ( failLog , phrase ) {
return true
}
}
numLines := strings . Count ( failLog , "\n" )
if numLines < 20 && strings . Contains ( failLog , "error: exit status" ) {
return true
}
2014-12-19 23:07:06 -07:00
// e.g. fatal: destination path 'go.tools.TMP' already exists and is not an empty directory.
// To be fixed in golang.org/issue/9407
if strings . Contains ( failLog , "fatal: destination path '" ) &&
strings . Contains ( failLog , "' already exists and is not an empty directory." ) {
return true
}
2014-12-14 20:12:14 -07:00
return false
}
func fullHash ( h string ) string {
if h == "" || len ( h ) == 40 {
return h
}
for _ , f := range failures ( ) {
if strings . HasPrefix ( f . Hash , h ) {
return f . Hash
}
}
log . Fatalf ( "invalid hash %q; failed to finds its full hash. Not a recent failure?" , h )
panic ( "unreachable" )
}
// hash may be empty
func wipe ( builder , hash string ) {
if hash != "" {
log . Printf ( "Clearing %s, hash %s" , builder , hash )
} else {
log . Printf ( "Clearing all builds for %s" , builder )
}
vals := url . Values {
"builder" : { builder } ,
"hash" : { hash } ,
"key" : { builderKey ( builder ) } ,
}
res , err := http . PostForm ( * builderPrefix + "/clear-results?" + vals . Encode ( ) , nil )
if err != nil {
log . Fatal ( err )
}
defer res . Body . Close ( )
if res . StatusCode != 200 {
log . Fatalf ( "Error clearing %v hash %q: %v" , builder , hash , res . Status )
}
}
func builderKey ( builder string ) string {
if v , ok := builderKeyFromMaster ( builder ) ; ok {
return v
}
if * keyFile == "" {
log . Fatalf ( "No --key specified for builder %s" , builder )
}
slurp , err := ioutil . ReadFile ( * keyFile )
if err != nil {
log . Fatalf ( "Error reading builder key %s: %v" , builder , err )
}
return strings . TrimSpace ( string ( slurp ) )
}
func builderKeyFromMaster ( builder string ) ( key string , ok bool ) {
if * masterKeyFile == "" {
return
}
slurp , err := ioutil . ReadFile ( * masterKeyFile )
if err != nil {
return
}
h := hmac . New ( md5 . New , bytes . TrimSpace ( slurp ) )
h . Write ( [ ] byte ( builder ) )
return fmt . Sprintf ( "%x" , h . Sum ( nil ) ) , true
}
var (
failMu sync . Mutex
failCache [ ] Failure
)
func failures ( ) ( ret [ ] Failure ) {
failMu . Lock ( )
ret = failCache
failMu . Unlock ( )
if ret != nil {
return
}
ret = [ ] Failure { } // non-nil
res , err := http . Get ( * builderPrefix + "/?mode=failures" )
if err != nil {
log . Fatal ( err )
}
defer res . Body . Close ( )
slurp , err := ioutil . ReadAll ( res . Body )
if err != nil {
log . Fatal ( err )
}
body := string ( slurp )
for _ , line := range strings . Split ( body , "\n" ) {
f := strings . Fields ( line )
if len ( f ) == 3 {
ret = append ( ret , Failure {
Hash : f [ 0 ] ,
Builder : f [ 1 ] ,
LogURL : f [ 2 ] ,
} )
}
}
failMu . Lock ( )
failCache = ret
failMu . Unlock ( )
return ret
}