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