Index: perf/go/db/db.go |
diff --git a/perf/go/db/db.go b/perf/go/db/db.go |
index 405d1201856501ab75168705138bb3247efcf9b5..39efb3b2983a24c0a32285b6c7b54940bd93f06a 100644 |
--- a/perf/go/db/db.go |
+++ b/perf/go/db/db.go |
@@ -9,66 +9,66 @@ import ( |
"github.com/golang/glog" |
_ "github.com/mattn/go-sqlite3" |
"skia.googlesource.com/buildbot.git/perf/go/metadata" |
+ "skia.googlesource.com/buildbot.git/perf/go/util" |
) |
var ( |
// DB is the sql database where we have commit and annotation information stored. |
DB *sql.DB = nil |
+ |
+ // Keeps track if we are connected to MySQL or SQLite |
+ isMySQL = true |
) |
const ( |
+ // Key of the password for the readwrite user. |
METADATA_KEY = "readwrite" |
+ |
+ // Path where the SQLite database is stored when running locally. |
+ SQLITE_DB_PATH = "./perf.db" |
+ |
+ // Template to generate the database connection string in production. |
+ // The IP address of the database is found here: |
+ // https://console.developers.google.com/project/31977622648/sql/instances/skiaperf/overview |
+ // And 3306 is the default port for MySQL. |
+ DB_CONN_TMPL = "readwrite:%s@tcp(173.194.104.24:3306)/skia?parseTime=true" |
) |
// Init must be called once before DB is used. |
// |
// Since it used glog, make sure it is also called after flag.Parse is called. |
-func Init() { |
- // Connect to MySQL server. First, get the password from the metadata server. |
- // See https://developers.google.com/compute/docs/metadata#custom. |
- password, err := metadata.Get(METADATA_KEY) |
- if err == nil { |
- // The IP address of the database is found here: |
- // https://console.developers.google.com/project/31977622648/sql/instances/skiaperf/overview |
- // And 3306 is the default port for MySQL. |
- DB, err = sql.Open("mysql", fmt.Sprintf("readwrite:%s@tcp(173.194.104.24:3306)/skia?parseTime=true", password)) |
+func Init(mysqlConnStr string) { |
+ // If there is a connection string then connect to the MySQL server. |
+ // This is for testing only. In production we get the relevant information |
+ // from the metadata server. |
+ var err error |
+ |
+ if mysqlConnStr != "" { |
+ glog.Infoln("Opening SQL database.") |
+ if DB, err = sql.Open("mysql", mysqlConnStr); err == nil { |
+ glog.Infoln("Sending Ping.") |
+ err = DB.Ping() |
+ } |
+ |
if err != nil { |
glog.Fatalln("Failed to open connection to SQL server:", err) |
} |
+ |
+ isMySQL = true |
} else { |
- glog.Infoln("Failed to find metadata, unable to connect to MySQL server (Expected when running locally):", err) |
+ // Open a local SQLite database instead. |
+ glog.Infof("Opening local sqlite database at: %s", SQLITE_DB_PATH) |
// Fallback to sqlite for local use. |
- DB, err = sql.Open("sqlite3", "./perf.db") |
+ DB, err = sql.Open("sqlite3", SQLITE_DB_PATH) |
if err != nil { |
glog.Fatalln("Failed to open:", err) |
} |
+ isMySQL = false |
+ } |
- sql := `CREATE TABLE clusters ( |
- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
- ts TIMESTAMP NOT NULL, |
- hash TEXT NOT NULL, |
- regression FLOAT NOT NULL, |
- cluster MEDIUMTEXT NOT NULL, |
- status TEXT NOT NULL, |
- message TEXT NOT NULL |
- )` |
- _, err = DB.Exec(sql) |
- glog.Infoln("Status creating sqlite table for clusters:", err) |
- |
- sql = `CREATE TABLE shortcuts ( |
- id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
- traces MEDIUMTEXT NOT NULL |
- )` |
- _, err = DB.Exec(sql) |
- glog.Infoln("Status creating sqlite table for shortcuts:", err) |
- |
- sql = `CREATE TABLE tries ( |
- issue VARCHAR(255) NOT NULL PRIMARY KEY, |
- lastUpdated TIMESTAMP NOT NULL, |
- results MEDIUMTEXT NOT NULL |
- )` |
- _, err = DB.Exec(sql) |
- glog.Infoln("Status creating sqlite table for tries:", err) |
+ // Make sure the migration table exists. |
+ if err := ensureVersionTable(); err != nil { |
+ glog.Fatalln("Unable to guarantee valid version table:", err.Error()) |
} |
// Ping the database to keep the connection fresh. |
@@ -82,3 +82,239 @@ func Init() { |
} |
}() |
} |
+ |
+// Returns the DB connection string for running in production where a |
+// metadata server is available. If 'local' is true it will always return |
+// "" (empty string). When used with Init() this will cause it to use a |
+// local SQLite database. If it's not local and the meta data server is |
+// unreachable it will terminate. |
+func ProdConnectionString(local bool) string { |
+ if local { |
+ return "" |
+ } |
+ |
+ // First, get the password from the metadata server. |
+ // See https://developers.google.com/compute/docs/metadata#custom. |
+ password, err := metadata.Get(METADATA_KEY) |
+ if err != nil { |
+ glog.Fatalf("Failed to find metadata. Use 'local' flag when running locally.") |
+ } |
+ return fmt.Sprintf(DB_CONN_TMPL, password) |
+} |
+ |
+func Migrate(targetVersion int) error { |
+ if (targetVersion < 0) || (targetVersion > MaxDBVersion()) { |
+ glog.Fatalf("Target db version must be in range: [0 .. %d]", MaxDBVersion()) |
+ } |
+ |
+ currentVersion, err := DBVersion() |
+ if err != nil { |
+ return err |
+ } |
+ |
+ if targetVersion == currentVersion { |
+ return nil |
+ } |
+ |
+ // start a transaction |
+ txn, err := DB.Begin() |
+ if err != nil { |
+ return err |
+ } |
+ defer func() { |
+ if err != nil { |
+ glog.Errorf("Rolling back comit. Error: %s", err) |
+ txn.Rollback() |
+ } else { |
+ glog.Infoln("Committing changes.") |
+ txn.Commit() |
+ } |
+ }() |
+ |
+ // run through the transactions |
+ runSteps := getMigrations(isMySQL, currentVersion, targetVersion) |
+ if len(runSteps) == 0 { |
+ glog.Fatalln("Unable to find migration steps.") |
+ } |
+ |
+ for _, step := range runSteps { |
+ for _, stmt := range step { |
+ glog.Infoln("EXECUTING: \n", stmt) |
+ if _, err = txn.Exec(stmt); err != nil { |
+ return err |
+ } |
+ } |
+ } |
+ |
+ // update the dbversion table |
+ if err = setDBVersion(txn, targetVersion); err != nil { |
+ return err |
+ } |
+ |
+ return nil |
+} |
+ |
+func DBVersion() (int, error) { |
+ stmt := ` |
+ SELECT version |
+ FROM sk_db_version |
+ WHERE id=1` |
+ |
+ var version int |
+ err := DB.QueryRow(stmt).Scan(&version) |
+ return version, err |
+} |
+ |
+func MaxDBVersion() int { |
+ return len(migrationSteps) |
+} |
+ |
+func setDBVersion(txn *sql.Tx, newDBVersion int) error { |
+ stmt := `REPLACE INTO sk_db_version (id, version, updated) VALUES(1, ?, ?)` |
+ _, err := txn.Exec(stmt, newDBVersion, time.Now().Unix()) |
+ return err |
+} |
+ |
+func ensureVersionTable() error { |
+ txn, err := DB.Begin() |
+ defer func() { |
+ if err != nil { |
+ glog.Errorf("Encountered error rolling back: %s", err.Error()) |
+ txn.Rollback() |
+ } else { |
+ glog.Infoln("Version table OK.") |
+ txn.Commit() |
+ } |
+ }() |
+ |
+ if err != nil { |
+ fmt.Errorf("Unable to start database transaction. %s", err.Error()) |
+ } |
+ |
+ stmt := `CREATE TABLE IF NOT EXISTS sk_db_version ( |
+ id INTEGER NOT NULL PRIMARY KEY, |
+ version INTEGER NOT NULL, |
+ updated BIGINT NOT NULL |
+ )` |
+ if _, err = txn.Exec(stmt); err != nil { |
+ return fmt.Errorf("Creating version table failed: %s", err.Error()) |
+ } |
+ |
+ stmt = "SELECT COUNT(*) FROM sk_db_version" |
+ var count int |
+ if err = txn.QueryRow(stmt).Scan(&count); err != nil { |
+ return fmt.Errorf("Unable to read version table: %s", err.Error()) |
+ } |
+ |
+ // In both cases we want the transaction to roll back. |
+ if count == 0 { |
+ err = setDBVersion(txn, 0) |
+ } else if count > 1 { |
+ err = fmt.Errorf("Version table contains more than one row.") |
+ } |
+ |
+ return err |
+} |
+ |
+// Returns the SQL statements base on whether we are using MySQL and the |
+// current and target DB version. |
+// This function assumes that currentVersion != targetVersion. |
+func getMigrations(isMySQL bool, currentVersion int, targetVersion int) [][]string { |
+ inc := util.SignInt(targetVersion - currentVersion) |
+ idx := currentVersion |
+ if inc < 0 { |
+ idx = currentVersion - 1 |
+ } |
+ delta := util.AbsInt(targetVersion - currentVersion) |
+ result := make([][]string, 0, delta) |
+ |
+ for i := 0; i < delta; i++ { |
+ var temp []string |
+ switch { |
+ // using mysqlp |
+ case (inc > 0) && isMySQL: |
+ temp = migrationSteps[idx].MySQLUp |
+ case (inc < 0) && isMySQL: |
+ temp = migrationSteps[idx].MySQLDown |
+ // using sqlite |
+ case (inc > 0): |
+ temp = migrationSteps[idx].SQLiteUp |
+ case (inc < 0): |
+ temp = migrationSteps[idx].SQLiteDown |
+ } |
+ result = append(result, temp) |
+ idx += inc |
+ } |
+ return result |
+} |
+ |
+// Define the migration steps. |
+// Note: Only add to this list, once a step has landed in version control it |
+// must not be changed. |
+var migrationSteps = []struct { |
+ MySQLUp []string |
+ MySQLDown []string |
+ SQLiteUp []string |
+ SQLiteDown []string |
+}{ |
+ // version 1 |
+ { |
+ MySQLUp: []string{ |
+ `CREATE TABLE IF NOT EXISTS shortcuts ( |
+ id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, |
+ traces MEDIUMTEXT NOT NULL |
+ )`, |
+ |
+ `CREATE TABLE IF NOT EXISTS clusters ( |
+ id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, |
+ ts BIGINT NOT NULL, |
+ hash TEXT NOT NULL, |
+ regression FLOAT NOT NULL, |
+ cluster MEDIUMTEXT NOT NULL, |
+ status TEXT NOT NULL, |
+ message TEXT NOT NULL |
+ )`, |
+ |
+ `CREATE TABLE IF NOT EXISTS tries ( |
+ issue VARCHAR(255) NOT NULL PRIMARY KEY, |
+ lastUpdated BIGINT NOT NULL, |
+ results LONGTEXT NOT NULL |
+ )`, |
+ }, |
+ MySQLDown: []string{}, |
+ SQLiteUp: []string{ |
+ `CREATE TABLE clusters ( |
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
+ ts TIMESTAMP NOT NULL, |
+ hash TEXT NOT NULL, |
+ regression FLOAT NOT NULL, |
+ cluster MEDIUMTEXT NOT NULL, |
+ status TEXT NOT NULL, |
+ message TEXT NOT NULL |
+ )`, |
+ `CREATE TABLE shortcuts ( |
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
+ traces MEDIUMTEXT NOT NULL |
+ )`, |
+ `CREATE TABLE tries ( |
+ issue VARCHAR(255) NOT NULL PRIMARY KEY, |
+ lastUpdated TIMESTAMP NOT NULL, |
+ results MEDIUMTEXT NOT NULL |
+ )`, |
+ }, |
+ SQLiteDown: []string{ |
+ `DROP TABLE IF EXISTS clusters`, |
+ `DROP TABLE IF EXISTS shortcuts`, |
+ `DROP TABLE IF EXISTS tries`, |
+ }, |
+ }, |
+ |
+ // Use this is a template for more migration steps. |
+ // version x |
+ // { |
+ // MySQLUp: , |
+ // MySQLDown: , |
+ // SQLiteUp: , |
+ // SQLiteDown: , |
+ // }, |
+} |