Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(561)

Side by Side Diff: perf/go/db/db.go

Issue 608273002: Add versioning to perf SQL database (Closed) Base URL: https://skia.googlesource.com/buildbot@master
Patch Set: Incorporated feedback Created 6 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698