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 |