Index: perf/go/db/db.go |
diff --git a/perf/go/db/db.go b/perf/go/db/db.go |
index 405d1201856501ab75168705138bb3247efcf9b5..bbd076280b8c60de674e798e46bcfaf294951ad5 100644 |
--- a/perf/go/db/db.go |
+++ b/perf/go/db/db.go |
@@ -9,66 +9,75 @@ 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" |
) |
+// Add any new migration to this list of functions |
+// NOTE: Do not change these once commited, but instead add a migration step. |
+var migrationSteps [](func() ([]string, []string, []string, []string)) |
jcgregorio
2014/09/30 14:54:08
Instead of having the var, which is initialized el
stephana
2014/09/30 18:11:49
Done.
|
+ |
// 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) { |
+ // Define the migration steps. |
+ migrationSteps = [](func() ([]string, []string, []string, []string)){ |
jcgregorio
2014/09/30 14:54:08
Move the initialization outside of here into the v
stephana
2014/09/30 18:11:49
Done.
|
+ migration_1, |
+ } |
+ |
+ // 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 +91,227 @@ func Init() { |
} |
}() |
} |
+ |
+// Returns the DB connection string for running in production where a |
+// metadata server is available. If the meta data server is not reachable |
+// it will return an empty string. If used with the Init() function this |
+// will result in a local SQLite database. |
jcgregorio
2014/09/30 14:54:08
What if we run in production and forget to set the
stephana
2014/09/30 18:11:49
I should behave exactly like it does now.
If you
jcgregorio
2014/09/30 18:26:00
Yeah, let's do that.
|
+func ProdConnectionString() string { |
+ // First, get the password from the metadata server. |
+ // See https://developers.google.com/compute/docs/metadata#custom. |
+ password, err := metadata.Get(METADATA_KEY) |
+ glog.Infoln("Failed to find metadata, unable to connect to MySQL server (Expected when running locally):", err) |
+ |
+ if err != nil { |
+ |
+ return "" |
+ } |
+ 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, txErr := DBVersion() |
jcgregorio
2014/09/30 14:54:08
errors should always be called err
stephana
2014/09/30 18:11:49
Done.
|
+ if txErr != nil { |
+ return txErr |
+ } |
+ |
+ if targetVersion == currentVersion { |
+ return nil |
+ } |
+ |
+ // start a transaction |
+ txn, txErr := DB.Begin() |
+ if txErr != nil { |
+ return txErr |
+ } |
+ defer func() { |
+ if txErr != nil { |
+ glog.Errorf("Rolling back comit. Error: %s", txErr) |
+ 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) |
+ _, txErr = txn.Exec(stmt) |
+ if txErr != nil { |
+ return txErr |
+ } |
+ } |
+ } |
+ |
+ // update the dbversion table |
+ txErr = setDBVersion(txn, targetVersion) |
+ if txErr != nil { |
+ return txErr |
+ } |
+ |
+ 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, txnErr := DB.Begin() |
jcgregorio
2014/09/30 14:54:08
txnErr -> err
stephana
2014/09/30 18:11:49
Done.
|
+ defer func() { |
+ if txnErr != nil { |
+ glog.Errorf("Encountered error rolling back: %s", txnErr.Error()) |
+ txn.Rollback() |
+ } else { |
+ glog.Infoln("Version table OK.") |
+ txn.Commit() |
+ } |
+ }() |
+ |
+ if txnErr != nil { |
+ fmt.Errorf("Unable to start database transaction. %s", txnErr.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 _, txnErr = txn.Exec(stmt); txnErr != nil { |
+ return fmt.Errorf("Creating version table failed: %s", txnErr.Error()) |
+ } |
+ |
+ var count int |
+ if txnErr = txn.QueryRow("SELECT COUNT(*) FROM sk_db_version").Scan(&count); txnErr != nil { |
+ return fmt.Errorf("Unable to read version table: %s", txnErr.Error()) |
+ } |
+ |
+ if count == 0 { |
+ if txnErr = setDBVersion(txn, 0); txnErr != nil { |
+ return txnErr |
+ } |
+ } else if count > 1 { |
+ return fmt.Errorf("Version table contains more than one row.") |
+ } |
+ |
+ return txnErr |
+} |
+ |
+// 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]() |
+ case (inc < 0) && isMySQL: |
+ _, _, _, temp = migrationSteps[idx]() |
+ // using sqlite |
+ case (inc > 0): |
+ temp, _, _, _ = migrationSteps[idx]() |
+ case (inc < 0): |
+ _, temp, _, _ = migrationSteps[idx]() |
+ } |
+ result = append(result, temp) |
+ idx += inc |
+ } |
+ return result |
+} |
+ |
+func migration_1() (sqLiteUp, sqLiteDown, mySQLUp, mySQLDown []string) { |
+ // SQLite statements to alter the datase. |
+ 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 |
+ )`, |
+ } |
+ |
+ // When using SQLite we fully reverse the previous step. |
+ sqLiteDown = []string{ |
+ `DROP TABLE IF EXISTS clusters`, |
+ `DROP TABLE IF EXISTS shortcuts`, |
+ `DROP TABLE IF EXISTS tries`, |
+ } |
+ |
+ 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 |
+ )`, |
+ } |
+ |
+ // We are not removing the tables to make sure we don't delete existing |
+ // data. This is just for the first migration. |
+ mySQLDown = []string{} |
+ return |
+} |