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

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: Incorporating feedback 4 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
« no previous file with comments | « perf/Makefile ('k') | perf/go/db/db_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
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 }
OLDNEW
« no previous file with comments | « perf/Makefile ('k') | perf/go/db/db_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698