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