Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 package db | 1 package db |
| 2 | 2 |
| 3 import ( | 3 import ( |
| 4 "database/sql" | 4 "database/sql" |
| 5 "fmt" | 5 "fmt" |
| 6 "time" | 6 "time" |
| 7 | 7 |
| 8 _ "github.com/go-sql-driver/mysql" | 8 _ "github.com/go-sql-driver/mysql" |
| 9 "github.com/golang/glog" | 9 "github.com/golang/glog" |
| 10 _ "github.com/mattn/go-sqlite3" | 10 _ "github.com/mattn/go-sqlite3" |
| 11 "skia.googlesource.com/buildbot.git/perf/go/metadata" | 11 "skia.googlesource.com/buildbot.git/perf/go/metadata" |
| 12 "skia.googlesource.com/buildbot.git/perf/go/util" | |
| 12 ) | 13 ) |
| 13 | 14 |
| 14 var ( | 15 var ( |
| 15 // DB is the sql database where we have commit and annotation informatio n stored. | 16 // DB is the sql database where we have commit and annotation informatio n stored. |
| 16 DB *sql.DB = nil | 17 DB *sql.DB = nil |
| 18 | |
| 19 // Keeps track if we are connected to MySQL or SQLite | |
| 20 isMySQL = true | |
| 17 ) | 21 ) |
| 18 | 22 |
| 19 const ( | 23 const ( |
| 24 // Key of the password for the readwrite user. | |
| 20 METADATA_KEY = "readwrite" | 25 METADATA_KEY = "readwrite" |
| 26 | |
| 27 // Path where the SQLite database is stored when running locally. | |
| 28 SQLITE_DB_PATH = "./perf.db" | |
| 29 | |
| 30 // Template to generate the database connection string in production. | |
| 31 // The IP address of the database is found here: | |
| 32 // https://console.developers.google.com/project/31977622648/sql/inst ances/skiaperf/overview | |
| 33 // And 3306 is the default port for MySQL. | |
| 34 DB_CONN_TMPL = "readwrite:%s@tcp(173.194.104.24:3306)/skia?parseTime=tru e" | |
| 21 ) | 35 ) |
| 22 | 36 |
| 37 // Add any new migration to this list of functions | |
| 38 // NOTE: Do not change these once commited, but instead add a migration step. | |
| 39 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.
| |
| 40 | |
| 23 // Init must be called once before DB is used. | 41 // Init must be called once before DB is used. |
| 24 // | 42 // |
| 25 // Since it used glog, make sure it is also called after flag.Parse is called. | 43 // Since it used glog, make sure it is also called after flag.Parse is called. |
| 26 func Init() { | 44 func Init(mysqlConnStr string) { |
| 27 » // Connect to MySQL server. First, get the password from the metadata se rver. | 45 » // Define the migration steps. |
| 28 » // See https://developers.google.com/compute/docs/metadata#custom. | 46 » 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.
| |
| 29 » password, err := metadata.Get(METADATA_KEY) | 47 » » migration_1, |
| 30 » if err == nil { | 48 » } |
| 31 » » // The IP address of the database is found here: | 49 |
| 32 » » // https://console.developers.google.com/project/31977622648/ sql/instances/skiaperf/overview | 50 » // If there is a connection string then connect to the MySQL server. |
| 33 » » // And 3306 is the default port for MySQL. | 51 » // This is for testing only. In production we get the relevant informati on |
| 34 » » DB, err = sql.Open("mysql", fmt.Sprintf("readwrite:%s@tcp(173.19 4.104.24:3306)/skia?parseTime=true", password)) | 52 » // from the metadata server. |
| 53 » var err error | |
| 54 | |
| 55 » if mysqlConnStr != "" { | |
| 56 » » glog.Infoln("Opening SQL database.") | |
| 57 » » if DB, err = sql.Open("mysql", mysqlConnStr); err == nil { | |
| 58 » » » glog.Infoln("Sending Ping.") | |
| 59 » » » err = DB.Ping() | |
| 60 » » } | |
| 61 | |
| 35 if err != nil { | 62 if err != nil { |
| 36 glog.Fatalln("Failed to open connection to SQL server:", err) | 63 glog.Fatalln("Failed to open connection to SQL server:", err) |
| 37 } | 64 } |
| 65 | |
| 66 isMySQL = true | |
| 38 } else { | 67 } else { |
| 39 » » glog.Infoln("Failed to find metadata, unable to connect to MySQL server (Expected when running locally):", err) | 68 » » // Open a local SQLite database instead. |
| 69 » » glog.Infof("Opening local sqlite database at: %s", SQLITE_DB_PAT H) | |
| 40 // Fallback to sqlite for local use. | 70 // Fallback to sqlite for local use. |
| 41 » » DB, err = sql.Open("sqlite3", "./perf.db") | 71 » » DB, err = sql.Open("sqlite3", SQLITE_DB_PATH) |
| 42 if err != nil { | 72 if err != nil { |
| 43 glog.Fatalln("Failed to open:", err) | 73 glog.Fatalln("Failed to open:", err) |
| 44 } | 74 } |
| 75 isMySQL = false | |
| 76 } | |
| 45 | 77 |
| 46 » » sql := `CREATE TABLE clusters ( | 78 » // Make sure the migration table exists. |
| 47 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | 79 » if err := ensureVersionTable(); err != nil { |
| 48 ts TIMESTAMP NOT NULL, | 80 » » glog.Fatalln("Unable to guarantee valid version table:", err.Err or()) |
| 49 hash TEXT NOT NULL, | |
| 50 regression FLOAT NOT NULL, | |
| 51 cluster MEDIUMTEXT NOT NULL, | |
| 52 status TEXT NOT NULL, | |
| 53 message TEXT NOT NULL | |
| 54 )` | |
| 55 » » _, err = DB.Exec(sql) | |
| 56 » » glog.Infoln("Status creating sqlite table for clusters:", err) | |
| 57 | |
| 58 » » sql = `CREATE TABLE shortcuts ( | |
| 59 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | |
| 60 traces MEDIUMTEXT NOT NULL | |
| 61 )` | |
| 62 » » _, err = DB.Exec(sql) | |
| 63 » » glog.Infoln("Status creating sqlite table for shortcuts:", err) | |
| 64 | |
| 65 » » sql = `CREATE TABLE tries ( | |
| 66 issue VARCHAR(255) NOT NULL PRIMARY KEY, | |
| 67 lastUpdated TIMESTAMP NOT NULL, | |
| 68 results MEDIUMTEXT NOT NULL | |
| 69 )` | |
| 70 » » _, err = DB.Exec(sql) | |
| 71 » » glog.Infoln("Status creating sqlite table for tries:", err) | |
| 72 } | 81 } |
| 73 | 82 |
| 74 // Ping the database to keep the connection fresh. | 83 // Ping the database to keep the connection fresh. |
| 75 go func() { | 84 go func() { |
| 76 c := time.Tick(1 * time.Minute) | 85 c := time.Tick(1 * time.Minute) |
| 77 for _ = range c { | 86 for _ = range c { |
| 78 if err := DB.Ping(); err != nil { | 87 if err := DB.Ping(); err != nil { |
| 79 glog.Warningln("Database failed to respond:", er r) | 88 glog.Warningln("Database failed to respond:", er r) |
| 80 } | 89 } |
| 81 glog.Infof("db: Successful ping") | 90 glog.Infof("db: Successful ping") |
| 82 } | 91 } |
| 83 }() | 92 }() |
| 84 } | 93 } |
| 94 | |
| 95 // Returns the DB connection string for running in production where a | |
| 96 // metadata server is available. If the meta data server is not reachable | |
| 97 // it will return an empty string. If used with the Init() function this | |
| 98 // 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.
| |
| 99 func ProdConnectionString() string { | |
| 100 // First, get the password from the metadata server. | |
| 101 // See https://developers.google.com/compute/docs/metadata#custom. | |
| 102 password, err := metadata.Get(METADATA_KEY) | |
| 103 glog.Infoln("Failed to find metadata, unable to connect to MySQL server (Expected when running locally):", err) | |
| 104 | |
| 105 if err != nil { | |
| 106 | |
| 107 return "" | |
| 108 } | |
| 109 return fmt.Sprintf(DB_CONN_TMPL, password) | |
| 110 } | |
| 111 | |
| 112 func Migrate(targetVersion int) error { | |
| 113 if (targetVersion < 0) || (targetVersion > MaxDBVersion()) { | |
| 114 glog.Fatalf("Target db version must be in range: [0 .. %d]", Max DBVersion()) | |
| 115 } | |
| 116 | |
| 117 currentVersion, txErr := DBVersion() | |
|
jcgregorio
2014/09/30 14:54:08
errors should always be called err
stephana
2014/09/30 18:11:49
Done.
| |
| 118 if txErr != nil { | |
| 119 return txErr | |
| 120 } | |
| 121 | |
| 122 if targetVersion == currentVersion { | |
| 123 return nil | |
| 124 } | |
| 125 | |
| 126 // start a transaction | |
| 127 txn, txErr := DB.Begin() | |
| 128 if txErr != nil { | |
| 129 return txErr | |
| 130 } | |
| 131 defer func() { | |
| 132 if txErr != nil { | |
| 133 glog.Errorf("Rolling back comit. Error: %s", txErr) | |
| 134 txn.Rollback() | |
| 135 } else { | |
| 136 glog.Infoln("Committing changes.") | |
| 137 txn.Commit() | |
| 138 } | |
| 139 }() | |
| 140 | |
| 141 // run through the transactions | |
| 142 runSteps := getMigrations(isMySQL, currentVersion, targetVersion) | |
| 143 if len(runSteps) == 0 { | |
| 144 glog.Fatalln("Unable to find migration steps.") | |
| 145 } | |
| 146 | |
| 147 for _, step := range runSteps { | |
| 148 for _, stmt := range step { | |
| 149 glog.Infoln("EXECUTING: \n", stmt) | |
| 150 _, txErr = txn.Exec(stmt) | |
| 151 if txErr != nil { | |
| 152 return txErr | |
| 153 } | |
| 154 } | |
| 155 } | |
| 156 | |
| 157 // update the dbversion table | |
| 158 txErr = setDBVersion(txn, targetVersion) | |
| 159 if txErr != nil { | |
| 160 return txErr | |
| 161 } | |
| 162 | |
| 163 return nil | |
| 164 } | |
| 165 | |
| 166 func DBVersion() (int, error) { | |
| 167 stmt := ` | |
| 168 SELECT version | |
| 169 FROM sk_db_version | |
| 170 WHERE id=1` | |
| 171 | |
| 172 var version int | |
| 173 err := DB.QueryRow(stmt).Scan(&version) | |
| 174 return version, err | |
| 175 } | |
| 176 | |
| 177 func MaxDBVersion() int { | |
| 178 return len(migrationSteps) | |
| 179 } | |
| 180 | |
| 181 func setDBVersion(txn *sql.Tx, newDBVersion int) error { | |
| 182 stmt := `REPLACE INTO sk_db_version (id, version, updated) VALUES(1, ?, ?)` | |
| 183 _, err := txn.Exec(stmt, newDBVersion, time.Now().Unix()) | |
| 184 return err | |
| 185 } | |
| 186 | |
| 187 func ensureVersionTable() error { | |
| 188 txn, txnErr := DB.Begin() | |
|
jcgregorio
2014/09/30 14:54:08
txnErr -> err
stephana
2014/09/30 18:11:49
Done.
| |
| 189 defer func() { | |
| 190 if txnErr != nil { | |
| 191 glog.Errorf("Encountered error rolling back: %s", txnErr .Error()) | |
| 192 txn.Rollback() | |
| 193 } else { | |
| 194 glog.Infoln("Version table OK.") | |
| 195 txn.Commit() | |
| 196 } | |
| 197 }() | |
| 198 | |
| 199 if txnErr != nil { | |
| 200 fmt.Errorf("Unable to start database transaction. %s", txnErr.Er ror()) | |
| 201 } | |
| 202 | |
| 203 stmt := `CREATE TABLE IF NOT EXISTS sk_db_version ( | |
| 204 id INTEGER NOT NULL PRIMARY KEY, | |
| 205 version INTEGER NOT NULL, | |
| 206 updated BIGINT NOT NULL | |
| 207 )` | |
| 208 if _, txnErr = txn.Exec(stmt); txnErr != nil { | |
| 209 return fmt.Errorf("Creating version table failed: %s", txnErr.Er ror()) | |
| 210 } | |
| 211 | |
| 212 var count int | |
| 213 if txnErr = txn.QueryRow("SELECT COUNT(*) FROM sk_db_version").Scan(&cou nt); txnErr != nil { | |
| 214 return fmt.Errorf("Unable to read version table: %s", txnErr.Err or()) | |
| 215 } | |
| 216 | |
| 217 if count == 0 { | |
| 218 if txnErr = setDBVersion(txn, 0); txnErr != nil { | |
| 219 return txnErr | |
| 220 } | |
| 221 } else if count > 1 { | |
| 222 return fmt.Errorf("Version table contains more than one row.") | |
| 223 } | |
| 224 | |
| 225 return txnErr | |
| 226 } | |
| 227 | |
| 228 // Returns the SQL statements base on whether we are using MySQL and the | |
| 229 // current and target DB version. | |
| 230 // This function assumes that currentVersion != targetVersion. | |
| 231 func getMigrations(isMySQL bool, currentVersion int, targetVersion int) [][]stri ng { | |
| 232 inc := util.SignInt(targetVersion - currentVersion) | |
| 233 idx := currentVersion | |
| 234 if inc < 0 { | |
| 235 idx = currentVersion - 1 | |
| 236 } | |
| 237 delta := util.AbsInt(targetVersion - currentVersion) | |
| 238 result := make([][]string, 0, delta) | |
| 239 | |
| 240 for i := 0; i < delta; i++ { | |
| 241 var temp []string | |
| 242 switch { | |
| 243 // using mysqlp | |
| 244 case (inc > 0) && isMySQL: | |
| 245 _, _, temp, _ = migrationSteps[idx]() | |
| 246 case (inc < 0) && isMySQL: | |
| 247 _, _, _, temp = migrationSteps[idx]() | |
| 248 // using sqlite | |
| 249 case (inc > 0): | |
| 250 temp, _, _, _ = migrationSteps[idx]() | |
| 251 case (inc < 0): | |
| 252 _, temp, _, _ = migrationSteps[idx]() | |
| 253 } | |
| 254 result = append(result, temp) | |
| 255 idx += inc | |
| 256 } | |
| 257 return result | |
| 258 } | |
| 259 | |
| 260 func migration_1() (sqLiteUp, sqLiteDown, mySQLUp, mySQLDown []string) { | |
| 261 // SQLite statements to alter the datase. | |
| 262 sqLiteUp = []string{ | |
| 263 `CREATE TABLE clusters ( | |
| 264 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT , | |
| 265 ts TIMESTAMP NOT NULL, | |
| 266 hash TEXT NOT NULL, | |
| 267 regression FLOAT NOT NULL, | |
| 268 cluster MEDIUMTEXT NOT NULL, | |
| 269 status TEXT NOT NULL, | |
| 270 message TEXT NOT NULL | |
| 271 )`, | |
| 272 `CREATE TABLE shortcuts ( | |
| 273 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | |
| 274 traces MEDIUMTEXT NOT NULL | |
| 275 )`, | |
| 276 `CREATE TABLE tries ( | |
| 277 issue VARCHAR(255) NOT NULL PRIMARY KEY, | |
| 278 lastUpdated TIMESTAMP NOT NULL, | |
| 279 results MEDIUMTEXT NOT NULL | |
| 280 )`, | |
| 281 } | |
| 282 | |
| 283 // When using SQLite we fully reverse the previous step. | |
| 284 sqLiteDown = []string{ | |
| 285 `DROP TABLE IF EXISTS clusters`, | |
| 286 `DROP TABLE IF EXISTS shortcuts`, | |
| 287 `DROP TABLE IF EXISTS tries`, | |
| 288 } | |
| 289 | |
| 290 mySQLUp = []string{ | |
| 291 `CREATE TABLE IF NOT EXISTS shortcuts ( | |
| 292 id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, | |
| 293 traces MEDIUMTEXT NOT NULL | |
| 294 )`, | |
| 295 | |
| 296 `CREATE TABLE IF NOT EXISTS clusters ( | |
| 297 id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, | |
| 298 ts BIGINT NOT NULL, | |
| 299 hash TEXT NOT NULL, | |
| 300 regression FLOAT NOT NULL, | |
| 301 cluster MEDIUMTEXT NOT NULL, | |
| 302 status TEXT NOT NULL, | |
| 303 message TEXT NOT NULL | |
| 304 )`, | |
| 305 | |
| 306 `CREATE TABLE IF NOT EXISTS tries ( | |
| 307 issue VARCHAR(255) NOT NULL PRIMARY KEY, | |
| 308 lastUpdated BIGINT NOT NULL, | |
| 309 results LONGTEXT NOT NULL | |
| 310 )`, | |
| 311 } | |
| 312 | |
| 313 // We are not removing the tables to make sure we don't delete existing | |
| 314 // data. This is just for the first migration. | |
| 315 mySQLDown = []string{} | |
| 316 return | |
| 317 } | |
| OLD | NEW |