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