vitess.io/vitess@v0.16.2/go/vt/vtgr/db/db.go (about)

     1  /*
     2     Copyright 2014 Outbrain Inc.
     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  /*
    18  	This file has been copied over from VTOrc package
    19  */
    20  
    21  package db
    22  
    23  import (
    24  	"database/sql"
    25  	"fmt"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	"vitess.io/vitess/go/vt/external/golib/sqlutils"
    31  	"vitess.io/vitess/go/vt/log"
    32  	"vitess.io/vitess/go/vt/vtgr/config"
    33  )
    34  
    35  var (
    36  	EmptyArgs []any
    37  	Db        DB = (*vtorcDB)(nil)
    38  )
    39  
    40  var mysqlURI string
    41  var dbMutex sync.Mutex
    42  
    43  type DB interface {
    44  	QueryOrchestrator(query string, argsArray []any, onRow func(sqlutils.RowMap) error) error
    45  }
    46  
    47  type vtorcDB struct {
    48  }
    49  
    50  var _ DB = (*vtorcDB)(nil)
    51  
    52  func (m *vtorcDB) QueryOrchestrator(query string, argsArray []any, onRow func(sqlutils.RowMap) error) error {
    53  	return QueryOrchestrator(query, argsArray, onRow)
    54  }
    55  
    56  type DummySQLResult struct {
    57  }
    58  
    59  func (dummyRes DummySQLResult) LastInsertId() (int64, error) {
    60  	return 0, nil
    61  }
    62  
    63  func (dummyRes DummySQLResult) RowsAffected() (int64, error) {
    64  	return 1, nil
    65  }
    66  
    67  func getMySQLURI() string {
    68  	dbMutex.Lock()
    69  	defer dbMutex.Unlock()
    70  	if mysqlURI != "" {
    71  		return mysqlURI
    72  	}
    73  	mysqlURI := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?timeout=%ds&readTimeout=%ds&rejectReadOnly=%t&interpolateParams=true",
    74  		config.Config.MySQLOrchestratorUser,
    75  		config.Config.MySQLOrchestratorPassword,
    76  		config.Config.MySQLOrchestratorHost,
    77  		config.Config.MySQLOrchestratorPort,
    78  		config.Config.MySQLOrchestratorDatabase,
    79  		config.Config.MySQLConnectTimeoutSeconds,
    80  		config.Config.MySQLOrchestratorReadTimeoutSeconds,
    81  		config.Config.MySQLOrchestratorRejectReadOnly,
    82  	)
    83  	if config.Config.MySQLOrchestratorUseMutualTLS {
    84  		mysqlURI, _ = SetupMySQLOrchestratorTLS(mysqlURI)
    85  	}
    86  	return mysqlURI
    87  }
    88  
    89  // OpenDiscovery returns a DB instance to access a topology instance.
    90  // It has lower read timeout than OpenTopology and is intended to
    91  // be used with low-latency discovery queries.
    92  func OpenDiscovery(host string, port int) (*sql.DB, error) {
    93  	return openTopology(host, port, config.Config.MySQLDiscoveryReadTimeoutSeconds)
    94  }
    95  
    96  // OpenTopology returns a DB instance to access a topology instance.
    97  func OpenTopology(host string, port int) (*sql.DB, error) {
    98  	return openTopology(host, port, config.Config.MySQLTopologyReadTimeoutSeconds)
    99  }
   100  
   101  func openTopology(host string, port int, readTimeout int) (db *sql.DB, err error) {
   102  	uri := fmt.Sprintf("%s:%s@tcp(%s:%d)/?timeout=%ds&readTimeout=%ds&interpolateParams=true",
   103  		config.Config.MySQLTopologyUser,
   104  		config.Config.MySQLTopologyPassword,
   105  		host, port,
   106  		config.Config.MySQLConnectTimeoutSeconds,
   107  		readTimeout,
   108  	)
   109  
   110  	if config.Config.MySQLTopologyUseMutualTLS ||
   111  		(config.Config.MySQLTopologyUseMixedTLS && requiresTLS(host, port, uri)) {
   112  		if uri, err = SetupMySQLTopologyTLS(uri); err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  	if db, _, err = sqlutils.GetDB(uri); err != nil {
   117  		return nil, err
   118  	}
   119  	if config.Config.MySQLConnectionLifetimeSeconds > 0 {
   120  		db.SetConnMaxLifetime(time.Duration(config.Config.MySQLConnectionLifetimeSeconds) * time.Second)
   121  	}
   122  	db.SetMaxOpenConns(config.MySQLTopologyMaxPoolConnections)
   123  	db.SetMaxIdleConns(config.MySQLTopologyMaxPoolConnections)
   124  	return db, err
   125  }
   126  
   127  func openOrchestratorMySQLGeneric() (db *sql.DB, fromCache bool, err error) {
   128  	uri := fmt.Sprintf("%s:%s@tcp(%s:%d)/?timeout=%ds&readTimeout=%ds&interpolateParams=true",
   129  		config.Config.MySQLOrchestratorUser,
   130  		config.Config.MySQLOrchestratorPassword,
   131  		config.Config.MySQLOrchestratorHost,
   132  		config.Config.MySQLOrchestratorPort,
   133  		config.Config.MySQLConnectTimeoutSeconds,
   134  		config.Config.MySQLOrchestratorReadTimeoutSeconds,
   135  	)
   136  	if config.Config.MySQLOrchestratorUseMutualTLS {
   137  		uri, _ = SetupMySQLOrchestratorTLS(uri)
   138  	}
   139  	return sqlutils.GetDB(uri)
   140  }
   141  
   142  func IsSQLite() bool {
   143  	return config.Config.IsSQLite()
   144  }
   145  
   146  // OpenTopology returns the DB instance for the orchestrator backed database
   147  func OpenOrchestrator() (db *sql.DB, err error) {
   148  	var fromCache bool
   149  	if IsSQLite() {
   150  		db, fromCache, err = sqlutils.GetSQLiteDB(config.Config.SQLite3DataFile)
   151  		if err == nil && !fromCache {
   152  			log.Infof("Connected to orchestrator backend: sqlite on %v", config.Config.SQLite3DataFile)
   153  		}
   154  		if db != nil {
   155  			db.SetMaxOpenConns(1)
   156  			db.SetMaxIdleConns(1)
   157  		}
   158  	} else {
   159  		if db, fromCache, err := openOrchestratorMySQLGeneric(); err != nil {
   160  			log.Errorf(err.Error())
   161  			return db, err
   162  		} else if !fromCache {
   163  			// first time ever we talk to MySQL
   164  			query := fmt.Sprintf("create database if not exists %s", config.Config.MySQLOrchestratorDatabase)
   165  			if _, err := db.Exec(query); err != nil {
   166  				log.Errorf(err.Error())
   167  				return db, err
   168  			}
   169  		}
   170  		db, fromCache, err = sqlutils.GetDB(getMySQLURI())
   171  		if err == nil && !fromCache {
   172  			// do not show the password but do show what we connect to.
   173  			safeMySQLURI := fmt.Sprintf("%s:?@tcp(%s:%d)/%s?timeout=%ds", config.Config.MySQLOrchestratorUser,
   174  				config.Config.MySQLOrchestratorHost, config.Config.MySQLOrchestratorPort, config.Config.MySQLOrchestratorDatabase, config.Config.MySQLConnectTimeoutSeconds)
   175  			log.Infof("Connected to orchestrator backend: %v", safeMySQLURI)
   176  			if config.Config.MySQLOrchestratorMaxPoolConnections > 0 {
   177  				log.Infof("Orchestrator pool SetMaxOpenConns: %d", config.Config.MySQLOrchestratorMaxPoolConnections)
   178  				db.SetMaxOpenConns(config.Config.MySQLOrchestratorMaxPoolConnections)
   179  			}
   180  			if config.Config.MySQLConnectionLifetimeSeconds > 0 {
   181  				db.SetConnMaxLifetime(time.Duration(config.Config.MySQLConnectionLifetimeSeconds) * time.Second)
   182  			}
   183  		}
   184  	}
   185  	if err == nil && !fromCache {
   186  		if !config.Config.SkipOrchestratorDatabaseUpdate {
   187  			initOrchestratorDB(db)
   188  		}
   189  		// A low value here will trigger reconnects which could
   190  		// make the number of backend connections hit the tcp
   191  		// limit. That's bad.  I could make this setting dynamic
   192  		// but then people need to know which value to use. For now
   193  		// allow up to 25% of MySQLOrchestratorMaxPoolConnections
   194  		// to be idle.  That should provide a good number which
   195  		// does not keep the maximum number of connections open but
   196  		// at the same time does not trigger disconnections and
   197  		// reconnections too frequently.
   198  		maxIdleConns := int(config.Config.MySQLOrchestratorMaxPoolConnections * 25 / 100)
   199  		if maxIdleConns < 10 {
   200  			maxIdleConns = 10
   201  		}
   202  		log.Infof("Connecting to backend %s:%d: maxConnections: %d, maxIdleConns: %d",
   203  			config.Config.MySQLOrchestratorHost,
   204  			config.Config.MySQLOrchestratorPort,
   205  			config.Config.MySQLOrchestratorMaxPoolConnections,
   206  			maxIdleConns)
   207  		db.SetMaxIdleConns(maxIdleConns)
   208  	}
   209  	return db, err
   210  }
   211  
   212  func translateStatement(statement string) (string, error) {
   213  	if IsSQLite() {
   214  		statement = sqlutils.ToSqlite3Dialect(statement)
   215  	}
   216  	return statement, nil
   217  }
   218  
   219  // versionIsDeployed checks if given version has already been deployed
   220  func versionIsDeployed(db *sql.DB) (result bool, err error) {
   221  	query := `
   222  		select
   223  			count(*) as is_deployed
   224  		from
   225  			orchestrator_db_deployments
   226  		where
   227  			deployed_version = ?
   228  		`
   229  	err = db.QueryRow(query, config.RuntimeCLIFlags.ConfiguredVersion).Scan(&result)
   230  	// err means the table 'orchestrator_db_deployments' does not even exist, in which case we proceed
   231  	// to deploy.
   232  	// If there's another error to this, like DB gone bad, then we're about to find out anyway.
   233  	return result, err
   234  }
   235  
   236  // registerOrchestratorDeployment updates the orchestrator_metadata table upon successful deployment
   237  func registerOrchestratorDeployment(db *sql.DB) error {
   238  	query := `
   239      	replace into orchestrator_db_deployments (
   240  				deployed_version, deployed_timestamp
   241  			) values (
   242  				?, NOW()
   243  			)
   244  				`
   245  	if _, err := execInternal(db, query, config.RuntimeCLIFlags.ConfiguredVersion); err != nil {
   246  		log.Fatalf("Unable to write to orchestrator_metadata: %+v", err)
   247  	}
   248  	log.Infof("Migrated database schema to version [%+v]", config.RuntimeCLIFlags.ConfiguredVersion)
   249  	return nil
   250  }
   251  
   252  // deployStatements will issue given sql queries that are not already known to be deployed.
   253  // This iterates both lists (to-run and already-deployed) and also verifies no contraditions.
   254  func deployStatements(db *sql.DB, queries []string) error {
   255  	tx, err := db.Begin()
   256  	if err != nil {
   257  		log.Fatal(err.Error())
   258  	}
   259  	// Ugly workaround ahead.
   260  	// Origin of this workaround is the existence of some "timestamp NOT NULL," column definitions,
   261  	// where in NO_ZERO_IN_DATE,NO_ZERO_DATE sql_mode are invalid (since default is implicitly "0")
   262  	// This means installation of orchestrator fails on such configured servers, and in particular on 5.7
   263  	// where this setting is the dfault.
   264  	// For purpose of backwards compatability, what we do is force sql_mode to be more relaxed, create the schemas
   265  	// along with the "invalid" definition, and then go ahead and fix those definitions via following ALTER statements.
   266  	// My bad.
   267  	originalSQLMode := ""
   268  	if config.Config.IsMySQL() {
   269  		_ = tx.QueryRow(`select @@session.sql_mode`).Scan(&originalSQLMode)
   270  		if _, err := tx.Exec(`set @@session.sql_mode=REPLACE(@@session.sql_mode, 'NO_ZERO_DATE', '')`); err != nil {
   271  			log.Fatal(err.Error())
   272  		}
   273  		if _, err := tx.Exec(`set @@session.sql_mode=REPLACE(@@session.sql_mode, 'NO_ZERO_IN_DATE', '')`); err != nil {
   274  			log.Fatal(err.Error())
   275  		}
   276  	}
   277  	for _, query := range queries {
   278  		query, err := translateStatement(query)
   279  		if err != nil {
   280  			log.Fatalf("Cannot initiate orchestrator: %+v; query=%+v", err, query)
   281  			return err
   282  		}
   283  		if _, err := tx.Exec(query); err != nil {
   284  			if strings.Contains(err.Error(), "syntax error") {
   285  				log.Fatalf("Cannot initiate orchestrator: %+v; query=%+v", err, query)
   286  				return err
   287  			}
   288  			if !sqlutils.IsAlterTable(query) && !sqlutils.IsCreateIndex(query) && !sqlutils.IsDropIndex(query) {
   289  				log.Fatalf("Cannot initiate orchestrator: %+v; query=%+v", err, query)
   290  				return err
   291  			}
   292  			if !strings.Contains(err.Error(), "duplicate column name") &&
   293  				!strings.Contains(err.Error(), "Duplicate column name") &&
   294  				!strings.Contains(err.Error(), "check that column/key exists") &&
   295  				!strings.Contains(err.Error(), "already exists") &&
   296  				!strings.Contains(err.Error(), "Duplicate key name") {
   297  				log.Errorf("Error initiating orchestrator: %+v; query=%+v", err, query)
   298  			}
   299  		}
   300  	}
   301  	if config.Config.IsMySQL() {
   302  		if _, err := tx.Exec(`set session sql_mode=?`, originalSQLMode); err != nil {
   303  			log.Fatal(err.Error())
   304  		}
   305  	}
   306  	if err := tx.Commit(); err != nil {
   307  		log.Fatal(err.Error())
   308  	}
   309  	return nil
   310  }
   311  
   312  // initOrchestratorDB attempts to create/upgrade the orchestrator backend database. It is created once in the
   313  // application's lifetime.
   314  func initOrchestratorDB(db *sql.DB) error {
   315  	log.Info("Initializing orchestrator")
   316  
   317  	versionAlreadyDeployed, err := versionIsDeployed(db)
   318  	if versionAlreadyDeployed && config.RuntimeCLIFlags.ConfiguredVersion != "" && err == nil {
   319  		// Already deployed with this version
   320  		return nil
   321  	}
   322  	if config.Config.PanicIfDifferentDatabaseDeploy && config.RuntimeCLIFlags.ConfiguredVersion != "" && !versionAlreadyDeployed {
   323  		log.Fatalf("PanicIfDifferentDatabaseDeploy is set. Configured version %s is not the version found in the database", config.RuntimeCLIFlags.ConfiguredVersion)
   324  	}
   325  	log.Info("Migrating database schema")
   326  	deployStatements(db, generateSQLBase)
   327  	deployStatements(db, generateSQLPatches)
   328  	registerOrchestratorDeployment(db)
   329  
   330  	if IsSQLite() {
   331  		ExecOrchestrator(`PRAGMA journal_mode = WAL`)
   332  		ExecOrchestrator(`PRAGMA synchronous = NORMAL`)
   333  	}
   334  
   335  	return nil
   336  }
   337  
   338  // execInternal
   339  func execInternal(db *sql.DB, query string, args ...any) (sql.Result, error) {
   340  	var err error
   341  	query, err = translateStatement(query)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	res, err := sqlutils.ExecNoPrepare(db, query, args...)
   346  	return res, err
   347  }
   348  
   349  // ExecOrchestrator will execute given query on the orchestrator backend database.
   350  func ExecOrchestrator(query string, args ...any) (sql.Result, error) {
   351  	var err error
   352  	query, err = translateStatement(query)
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	db, err := OpenOrchestrator()
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	res, err := sqlutils.ExecNoPrepare(db, query, args...)
   361  	return res, err
   362  }
   363  
   364  // QueryOrchestrator
   365  func QueryOrchestrator(query string, argsArray []any, onRow func(sqlutils.RowMap) error) error {
   366  	query, err := translateStatement(query)
   367  	if err != nil {
   368  		log.Fatalf("Cannot query orchestrator: %+v; query=%+v", err, query)
   369  		return err
   370  	}
   371  	db, err := OpenOrchestrator()
   372  	if err != nil {
   373  		return err
   374  	}
   375  
   376  	if err = sqlutils.QueryRowsMap(db, query, onRow, argsArray...); err != nil {
   377  		log.Warning(err.Error())
   378  	}
   379  
   380  	return err
   381  }