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 |