vitess.io/vitess@v0.16.2/go/vt/sidecardb/sidecardb.go (about) 1 /* 2 Copyright 2023 The Vitess Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package sidecardb 18 19 import ( 20 "context" 21 "embed" 22 "fmt" 23 "io/fs" 24 "path/filepath" 25 "regexp" 26 "runtime" 27 "strings" 28 "sync" 29 30 "vitess.io/vitess/go/history" 31 "vitess.io/vitess/go/mysql" 32 33 "vitess.io/vitess/go/mysql/fakesqldb" 34 35 vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" 36 "vitess.io/vitess/go/vt/sqlparser" 37 "vitess.io/vitess/go/vt/vterrors" 38 39 "vitess.io/vitess/go/stats" 40 41 "vitess.io/vitess/go/sqltypes" 42 "vitess.io/vitess/go/vt/log" 43 "vitess.io/vitess/go/vt/schemadiff" 44 ) 45 46 const ( 47 SidecarDBName = "_vt" 48 CreateSidecarDatabaseQuery = "create database if not exists _vt" 49 UseSidecarDatabaseQuery = "use _vt" 50 ShowSidecarDatabasesQuery = "SHOW DATABASES LIKE '\\_vt'" 51 SelectCurrentDatabaseQuery = "select database()" 52 ShowCreateTableQuery = "show create table _vt.%s" 53 54 CreateTableRegexp = "CREATE TABLE .* `\\_vt`\\..*" 55 AlterTableRegexp = "ALTER TABLE `\\_vt`\\..*" 56 ) 57 58 // All tables needed in the sidecar database have their schema in the schema subdirectory. 59 // 60 //go:embed schema/* 61 var schemaLocation embed.FS 62 63 type sidecarTable struct { 64 module string // which module uses this table 65 path string // path of the schema relative to this module 66 name string // table name 67 schema string // create table dml 68 } 69 70 func (t *sidecarTable) String() string { 71 return fmt.Sprintf("%s.%s (%s)", SidecarDBName, t.name, t.module) 72 } 73 74 var sidecarTables []*sidecarTable 75 var ddlCount *stats.Counter 76 var ddlErrorCount *stats.Counter 77 var ddlErrorHistory *history.History 78 var mu sync.Mutex 79 80 type ddlError struct { 81 tableName string 82 err error 83 } 84 85 const maxDDLErrorHistoryLength = 100 86 87 // failOnSchemaInitError decides whether we fail the schema init process when we encounter an error while 88 // applying a table schema upgrade DDL or continue with the next table. 89 // If true, tablets will not launch. The cluster will not come up until the issue is resolved. 90 // If false, the init process will continue trying to upgrade other tables. So some functionality might be broken 91 // due to an incorrect schema, but the cluster should come up and serve queries. 92 // This is an operational trade-off: if we always fail it could cause a major incident since the entire cluster will be down. 93 // If we are more permissive, it could cause hard-to-detect errors, because a module 94 // doesn't load or behaves incorrectly due to an incomplete upgrade. Errors however will be reported and if the 95 // related stats endpoints are monitored we should be able to diagnose/get alerted in a timely fashion. 96 const failOnSchemaInitError = false 97 98 const StatsKeyPrefix = "SidecarDBDDL" 99 const StatsKeyQueryCount = StatsKeyPrefix + "QueryCount" 100 const StatsKeyErrorCount = StatsKeyPrefix + "ErrorCount" 101 const StatsKeyErrors = StatsKeyPrefix + "Errors" 102 103 func init() { 104 initSchemaFiles() 105 ddlCount = stats.NewCounter(StatsKeyQueryCount, "Number of queries executed") 106 ddlErrorCount = stats.NewCounter(StatsKeyErrorCount, "Number of errors during sidecar schema upgrade") 107 ddlErrorHistory = history.New(maxDDLErrorHistoryLength) 108 stats.Publish(StatsKeyErrors, stats.StringMapFunc(func() map[string]string { 109 mu.Lock() 110 defer mu.Unlock() 111 result := make(map[string]string, len(ddlErrorHistory.Records())) 112 for _, e := range ddlErrorHistory.Records() { 113 d, ok := e.(*ddlError) 114 if ok { 115 result[d.tableName] = d.err.Error() 116 } 117 } 118 return result 119 })) 120 } 121 122 func validateSchemaDefinition(name, schema string) (string, error) { 123 stmt, err := sqlparser.ParseStrictDDL(schema) 124 125 if err != nil { 126 return "", err 127 } 128 createTable, ok := stmt.(*sqlparser.CreateTable) 129 if !ok { 130 return "", vterrors.Errorf(vtrpcpb.Code_INTERNAL, "expected CREATE TABLE. Got %v", sqlparser.CanonicalString(stmt)) 131 } 132 tableName := createTable.Table.Name.String() 133 qualifier := createTable.Table.Qualifier.String() 134 if qualifier != SidecarDBName { 135 return "", vterrors.Errorf(vtrpcpb.Code_INTERNAL, "database qualifier specified for the %s table is %s rather than the expected value of %s", 136 name, qualifier, SidecarDBName) 137 } 138 if !strings.EqualFold(tableName, name) { 139 return "", vterrors.Errorf(vtrpcpb.Code_INTERNAL, "table name of %s does not match the table name specified within the file: %s", name, tableName) 140 } 141 if !createTable.IfNotExists { 142 return "", vterrors.Errorf(vtrpcpb.Code_NOT_FOUND, "%s file did not include the required IF NOT EXISTS clause in the CREATE TABLE statement for the %s table", name, tableName) 143 } 144 normalizedSchema := sqlparser.CanonicalString(createTable) 145 return normalizedSchema, nil 146 } 147 148 func initSchemaFiles() { 149 sqlFileExtension := ".sql" 150 err := fs.WalkDir(schemaLocation, ".", func(path string, entry fs.DirEntry, err error) error { 151 if err != nil { 152 return err 153 } 154 if !entry.IsDir() { 155 var module string 156 dir, fname := filepath.Split(path) 157 if !strings.HasSuffix(strings.ToLower(fname), sqlFileExtension) { 158 log.Infof("Ignoring non-SQL file: %s, found during sidecar database initialization", path) 159 return nil 160 } 161 dirparts := strings.Split(strings.Trim(dir, "/"), "/") 162 switch len(dirparts) { 163 case 1: 164 module = dir 165 case 2: 166 module = fmt.Sprintf("%s/%s", dirparts[0], dirparts[1]) 167 default: 168 return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "unexpected path value of %s specified for sidecar schema table; expected structure is <module>[/<submodule>]/<tablename>.sql", dir) 169 } 170 171 name := strings.Split(fname, ".")[0] 172 schema, err := schemaLocation.ReadFile(path) 173 if err != nil { 174 panic(err) 175 } 176 var normalizedSchema string 177 if normalizedSchema, err = validateSchemaDefinition(name, string(schema)); err != nil { 178 return err 179 } 180 sidecarTables = append(sidecarTables, &sidecarTable{name: name, module: module, path: path, schema: normalizedSchema}) 181 } 182 return nil 183 }) 184 if err != nil { 185 log.Errorf("error loading schema files: %+v", err) 186 } 187 } 188 189 // printCallerDetails is a helper for dev debugging. 190 func printCallerDetails() { 191 pc, _, line, ok := runtime.Caller(2) 192 details := runtime.FuncForPC(pc) 193 if ok && details != nil { 194 log.Infof("%s schema init called from %s:%d\n", SidecarDBName, details.Name(), line) 195 } 196 } 197 198 type schemaInit struct { 199 ctx context.Context 200 exec Exec 201 existingTables map[string]bool 202 dbCreated bool // The first upgrade/create query will also create the sidecar database if required. 203 } 204 205 // Exec is a callback that has to be passed to Init() to execute the specified query in the database. 206 type Exec func(ctx context.Context, query string, maxRows int, useDB bool) (*sqltypes.Result, error) 207 208 // GetDDLCount returns the count of sidecardb DDLs that have been run as part of this vttablet's init process. 209 func GetDDLCount() int64 { 210 return ddlCount.Get() 211 } 212 213 // GetDDLErrorCount returns the count of sidecardb DDLs that have been errored out as part of this vttablet's init process. 214 func GetDDLErrorCount() int64 { 215 return ddlErrorCount.Get() 216 } 217 218 // GetDDLErrorHistory returns the errors encountered as part of this vttablet's init process.. 219 func GetDDLErrorHistory() []*ddlError { 220 var errors []*ddlError 221 for _, e := range ddlErrorHistory.Records() { 222 ddle, ok := e.(*ddlError) 223 if ok { 224 errors = append(errors, ddle) 225 } 226 } 227 return errors 228 } 229 230 // Init creates or upgrades the sidecar database based on declarative schema for all tables in the schema. 231 func Init(ctx context.Context, exec Exec) error { 232 printCallerDetails() // for debug purposes only, remove in v17 233 log.Infof("Starting sidecardb.Init()") 234 si := &schemaInit{ 235 ctx: ctx, 236 exec: exec, 237 } 238 239 // There are paths in the tablet initialization where we are in read-only mode but the schema is already updated. 240 // Hence, we should not always try to create the database, since it will then error out as the db is read-only. 241 dbExists, err := si.doesSidecarDBExist() 242 if err != nil { 243 return err 244 } 245 if !dbExists { 246 if err := si.createSidecarDB(); err != nil { 247 return err 248 } 249 si.dbCreated = true 250 } 251 252 if _, err := si.setCurrentDatabase(SidecarDBName); err != nil { 253 return err 254 } 255 256 resetSQLMode, err := si.setPermissiveSQLMode() 257 if err != nil { 258 return err 259 } 260 defer resetSQLMode() 261 262 for _, table := range sidecarTables { 263 if err := si.ensureSchema(table); err != nil { 264 return err 265 } 266 } 267 return nil 268 } 269 270 // setPermissiveSQLMode gets the current sql_mode for the session, removes any 271 // restrictions, and returns a function to restore it back to the original session value. 272 // We need to allow for the recreation of any data that currently exists in the table, such 273 // as e.g. allowing any zero dates that may already exist in a preexisting sidecar table. 274 func (si *schemaInit) setPermissiveSQLMode() (func(), error) { 275 rs, err := si.exec(si.ctx, `select @@session.sql_mode as sql_mode`, 1, false) 276 if err != nil { 277 return nil, vterrors.Errorf(vtrpcpb.Code_UNKNOWN, "could not read sql_mode: %v", err) 278 } 279 sqlMode, err := rs.Named().Row().ToString("sql_mode") 280 if err != nil { 281 return nil, vterrors.Errorf(vtrpcpb.Code_UNKNOWN, "could not read sql_mode: %v", err) 282 } 283 284 resetSQLModeFunc := func() { 285 restoreSQLModeQuery := fmt.Sprintf("set @@session.sql_mode='%s'", sqlMode) 286 _, _ = si.exec(si.ctx, restoreSQLModeQuery, 0, false) 287 } 288 289 if _, err := si.exec(si.ctx, "set @@session.sql_mode=''", 0, false); err != nil { 290 return nil, vterrors.Errorf(vtrpcpb.Code_UNKNOWN, "could not change sql_mode: %v", err) 291 } 292 return resetSQLModeFunc, nil 293 } 294 295 func (si *schemaInit) doesSidecarDBExist() (bool, error) { 296 rs, err := si.exec(si.ctx, ShowSidecarDatabasesQuery, 2, false) 297 if err != nil { 298 log.Error(err) 299 return false, err 300 } 301 302 switch len(rs.Rows) { 303 case 0: 304 log.Infof("doesSidecarDBExist: not found") 305 return false, nil 306 case 1: 307 log.Infof("doesSidecarDBExist: found") 308 return true, nil 309 default: 310 log.Errorf("found too many rows for sidecarDB %s: %d", SidecarDBName, len(rs.Rows)) 311 return false, vterrors.Errorf(vtrpcpb.Code_INTERNAL, "found too many rows for sidecarDB %s: %d", SidecarDBName, len(rs.Rows)) 312 } 313 } 314 315 func (si *schemaInit) createSidecarDB() error { 316 _, err := si.exec(si.ctx, CreateSidecarDatabaseQuery, 1, false) 317 if err != nil { 318 log.Error(err) 319 return err 320 } 321 log.Infof("createSidecarDB: %s", CreateSidecarDatabaseQuery) 322 return nil 323 } 324 325 // Sets db of current connection, returning the currently selected database. 326 func (si *schemaInit) setCurrentDatabase(dbName string) (string, error) { 327 rs, err := si.exec(si.ctx, SelectCurrentDatabaseQuery, 1, false) 328 if err != nil { 329 return "", err 330 } 331 if rs == nil || rs.Rows == nil { // we get this in tests 332 return "", nil 333 } 334 currentDB := rs.Rows[0][0].ToString() 335 if currentDB != "" { // while running tests we can get currentDB as empty 336 _, err = si.exec(si.ctx, fmt.Sprintf("use %s", dbName), 1, false) 337 if err != nil { 338 return "", err 339 } 340 } 341 return currentDB, nil 342 } 343 344 // Gets existing schema of a table in the sidecar database. 345 func (si *schemaInit) getCurrentSchema(tableName string) (string, error) { 346 var currentTableSchema string 347 348 rs, err := si.exec(si.ctx, fmt.Sprintf(ShowCreateTableQuery, tableName), 1, false) 349 if err != nil { 350 if sqlErr, ok := err.(*mysql.SQLError); ok && sqlErr.Number() == mysql.ERNoSuchTable { 351 // table does not exist in the sidecar database 352 return "", nil 353 } 354 log.Errorf("Error getting table schema for %s: %+v", tableName, err) 355 return "", err 356 } 357 if len(rs.Rows) > 0 { 358 currentTableSchema = rs.Rows[0][1].ToString() 359 } 360 return currentTableSchema, nil 361 } 362 363 // findTableSchemaDiff gets the diff that needs to be applied to current table schema to get the desired one. Will be an empty string if they match. 364 // This could be a CREATE statement if the table does not exist or an ALTER if table exists but has a different schema. 365 func (si *schemaInit) findTableSchemaDiff(tableName, current, desired string) (string, error) { 366 hints := &schemadiff.DiffHints{ 367 TableCharsetCollateStrategy: schemadiff.TableCharsetCollateIgnoreAlways, 368 AlterTableAlgorithmStrategy: schemadiff.AlterTableAlgorithmStrategyCopy, 369 } 370 diff, err := schemadiff.DiffCreateTablesQueries(current, desired, hints) 371 if err != nil { 372 return "", err 373 } 374 375 var ddl string 376 if diff != nil { 377 ddl = diff.CanonicalStatementString() 378 379 // Temporary logging to debug any eventual issues around the new schema init, should be removed in v17. 380 log.Infof("Current schema for table %s:\n%s", tableName, current) 381 if ddl == "" { 382 log.Infof("No changes needed for table %s", tableName) 383 } else { 384 log.Infof("Applying DDL for table %s:\n%s", tableName, ddl) 385 } 386 } 387 388 return ddl, nil 389 } 390 391 // ensureSchema first checks if the table exist, in which case it runs the create script provided in 392 // the schema directory. If the table exists, schemadiff is used to compare the existing schema with the desired one. 393 // If it needs to be altered then we run the alter script. 394 func (si *schemaInit) ensureSchema(table *sidecarTable) error { 395 ctx := si.ctx 396 desiredTableSchema := table.schema 397 398 var ddl string 399 currentTableSchema, err := si.getCurrentSchema(table.name) 400 if err != nil { 401 return err 402 } 403 ddl, err = si.findTableSchemaDiff(table.name, currentTableSchema, desiredTableSchema) 404 if err != nil { 405 return err 406 } 407 408 if ddl != "" { 409 if !si.dbCreated { 410 // We use CreateSidecarDatabaseQuery to also create the first binlog entry when a primary comes up. 411 // That statement doesn't make it to the replicas, so we run the query again so that it is replicated 412 // to the replicas so that the replicas can create the sidecar database. 413 if err := si.createSidecarDB(); err != nil { 414 return err 415 } 416 si.dbCreated = true 417 } 418 _, err := si.exec(ctx, ddl, 1, true) 419 if err != nil { 420 ddlErr := vterrors.Wrapf(err, 421 "Error running DDL %s for table %s during sidecar database initialization", ddl, table) 422 recordDDLError(table.name, ddlErr) 423 if failOnSchemaInitError { 424 return ddlErr 425 } 426 return nil 427 } 428 log.Infof("Applied DDL %s for table %s during sidecar database initialization", ddl, table) 429 ddlCount.Add(1) 430 return nil 431 } 432 log.Infof("Table schema was already up to date for the %s table in the %s sidecar database", table.name, SidecarDBName) 433 return nil 434 } 435 436 func recordDDLError(tableName string, err error) { 437 log.Error(err) 438 ddlErrorCount.Add(1) 439 ddlErrorHistory.Add(&ddlError{ 440 tableName: tableName, 441 err: err, 442 }) 443 } 444 445 // region unit-test-only 446 // This section uses helpers used in tests, but also in the go/vt/vtexplain/vtexplain_vttablet.go. 447 // Hence, it is here and not in the _test.go file. 448 449 // Query patterns to handle in mocks. 450 var sidecarDBInitQueries = []string{ 451 ShowSidecarDatabasesQuery, 452 SelectCurrentDatabaseQuery, 453 CreateSidecarDatabaseQuery, 454 UseSidecarDatabaseQuery, 455 } 456 457 var sidecarDBInitQueryPatterns = []string{ 458 CreateTableRegexp, 459 AlterTableRegexp, 460 } 461 462 // AddSchemaInitQueries adds sidecar database schema related queries to a mock db. 463 func AddSchemaInitQueries(db *fakesqldb.DB, populateTables bool) { 464 result := &sqltypes.Result{} 465 for _, q := range sidecarDBInitQueryPatterns { 466 db.AddQueryPattern(q, result) 467 } 468 for _, q := range sidecarDBInitQueries { 469 db.AddQuery(q, result) 470 } 471 for _, table := range sidecarTables { 472 result = &sqltypes.Result{} 473 if populateTables { 474 result = sqltypes.MakeTestResult(sqltypes.MakeTestFields( 475 "Table|Create Table", 476 "varchar|varchar"), 477 fmt.Sprintf("%s|%s", table.name, table.schema), 478 ) 479 } 480 db.AddQuery(fmt.Sprintf(ShowCreateTableQuery, table.name), result) 481 } 482 483 sqlModeResult := sqltypes.MakeTestResult(sqltypes.MakeTestFields( 484 "sql_mode", 485 "varchar"), 486 "ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION", 487 ) 488 db.AddQuery("select @@session.sql_mode as sql_mode", sqlModeResult) 489 490 db.AddQuery("set @@session.sql_mode=''", &sqltypes.Result{}) 491 } 492 493 // MatchesInitQuery returns true if query has one of the test patterns as a substring, or it matches a provided regexp. 494 func MatchesInitQuery(query string) bool { 495 query = strings.ToLower(query) 496 for _, q := range sidecarDBInitQueries { 497 if strings.EqualFold(q, query) { 498 return true 499 } 500 } 501 for _, q := range sidecarDBInitQueryPatterns { 502 q = strings.ToLower(q) 503 if strings.Contains(query, q) { 504 return true 505 } 506 if match, _ := regexp.MatchString(q, query); match { 507 return true 508 } 509 } 510 return false 511 } 512 513 // endregion