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

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: Clean up based on review in rietveld 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"
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 }
OLDNEW
« perf/db/reset_mysqldb.sh ('K') | « perf/db/reset_mysqldb.sh ('k') | perf/go/db/db_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698