vitess.io/vitess@v0.16.2/go/vt/mysqlctl/query.go (about)

     1  /*
     2  Copyright 2019 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 mysqlctl
    18  
    19  import (
    20  	"fmt"
    21  	"strings"
    22  	"time"
    23  
    24  	"context"
    25  
    26  	"vitess.io/vitess/go/mysql"
    27  	"vitess.io/vitess/go/sqltypes"
    28  	"vitess.io/vitess/go/vt/dbconnpool"
    29  	"vitess.io/vitess/go/vt/log"
    30  )
    31  
    32  // getPoolReconnect gets a connection from a pool, tests it, and reconnects if
    33  // the connection is lost.
    34  func getPoolReconnect(ctx context.Context, pool *dbconnpool.ConnectionPool) (*dbconnpool.PooledDBConnection, error) {
    35  	conn, err := pool.Get(ctx)
    36  	if err != nil {
    37  		return conn, err
    38  	}
    39  	// Run a test query to see if this connection is still good.
    40  	if _, err := conn.ExecuteFetch("SELECT 1", 1, false); err != nil {
    41  		// If we get a connection error, try to reconnect.
    42  		if sqlErr, ok := err.(*mysql.SQLError); ok && (sqlErr.Number() == mysql.CRServerGone || sqlErr.Number() == mysql.CRServerLost) {
    43  			if err := conn.Reconnect(ctx); err != nil {
    44  				conn.Recycle()
    45  				return nil, err
    46  			}
    47  			return conn, nil
    48  		}
    49  		conn.Recycle()
    50  		return nil, err
    51  	}
    52  	return conn, nil
    53  }
    54  
    55  // ExecuteSuperQuery allows the user to execute a query as a super user.
    56  func (mysqld *Mysqld) ExecuteSuperQuery(ctx context.Context, query string) error {
    57  	return mysqld.ExecuteSuperQueryList(ctx, []string{query})
    58  }
    59  
    60  // ExecuteSuperQueryList alows the user to execute queries as a super user.
    61  func (mysqld *Mysqld) ExecuteSuperQueryList(ctx context.Context, queryList []string) error {
    62  	conn, err := getPoolReconnect(ctx, mysqld.dbaPool)
    63  	if err != nil {
    64  		return err
    65  	}
    66  	defer conn.Recycle()
    67  
    68  	return mysqld.executeSuperQueryListConn(ctx, conn, queryList)
    69  }
    70  
    71  func limitString(s string, limit int) string {
    72  	if len(s) > limit {
    73  		return s[:limit]
    74  	}
    75  	return s
    76  }
    77  
    78  func (mysqld *Mysqld) executeSuperQueryListConn(ctx context.Context, conn *dbconnpool.PooledDBConnection, queryList []string) error {
    79  	const LogQueryLengthLimit = 200
    80  	for _, query := range queryList {
    81  		log.Infof("exec %s", limitString(redactPassword(query), LogQueryLengthLimit))
    82  		if _, err := mysqld.executeFetchContext(ctx, conn, query, 10000, false); err != nil {
    83  			log.Errorf("ExecuteFetch(%v) failed: %v", redactPassword(query), redactPassword(err.Error()))
    84  			return fmt.Errorf("ExecuteFetch(%v) failed: %v", redactPassword(query), redactPassword(err.Error()))
    85  		}
    86  	}
    87  	return nil
    88  }
    89  
    90  // FetchSuperQuery returns the results of executing a query as a super user.
    91  func (mysqld *Mysqld) FetchSuperQuery(ctx context.Context, query string) (*sqltypes.Result, error) {
    92  	conn, connErr := getPoolReconnect(ctx, mysqld.dbaPool)
    93  	if connErr != nil {
    94  		return nil, connErr
    95  	}
    96  	defer conn.Recycle()
    97  	log.V(6).Infof("fetch %v", query)
    98  	qr, err := mysqld.executeFetchContext(ctx, conn, query, 10000, true)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	return qr, nil
   103  }
   104  
   105  // executeFetchContext calls ExecuteFetch() on the given connection,
   106  // while respecting Context deadline and cancellation.
   107  func (mysqld *Mysqld) executeFetchContext(ctx context.Context, conn *dbconnpool.PooledDBConnection, query string, maxrows int, wantfields bool) (*sqltypes.Result, error) {
   108  	// Fast fail if context is done.
   109  	select {
   110  	case <-ctx.Done():
   111  		return nil, ctx.Err()
   112  	default:
   113  	}
   114  
   115  	// Execute asynchronously so we can select on both it and the context.
   116  	var qr *sqltypes.Result
   117  	var executeErr error
   118  	done := make(chan struct{})
   119  	go func() {
   120  		defer close(done)
   121  
   122  		qr, executeErr = conn.ExecuteFetch(query, maxrows, wantfields)
   123  	}()
   124  
   125  	// Wait for either the query or the context to be done.
   126  	select {
   127  	case <-done:
   128  		return qr, executeErr
   129  	case <-ctx.Done():
   130  		// If both are done already, we may end up here anyway because select
   131  		// chooses among multiple ready channels pseudorandomly.
   132  		// Check the done channel and prefer that one if it's ready.
   133  		select {
   134  		case <-done:
   135  			return qr, executeErr
   136  		default:
   137  		}
   138  
   139  		// The context expired or was cancelled.
   140  		// Try to kill the connection to effectively cancel the ExecuteFetch().
   141  		connID := conn.ID()
   142  		log.Infof("Mysqld.executeFetchContext(): killing connID %v due to timeout of query: %v", connID, query)
   143  		if killErr := mysqld.killConnection(connID); killErr != nil {
   144  			// Log it, but go ahead and wait for the query anyway.
   145  			log.Warningf("Mysqld.executeFetchContext(): failed to kill connID %v: %v", connID, killErr)
   146  		}
   147  		// Wait for the conn.ExecuteFetch() call to return.
   148  		<-done
   149  		// Close the connection. Upon Recycle() it will be thrown out.
   150  		conn.Close()
   151  		// ExecuteFetch() may have succeeded before we tried to kill it.
   152  		// If ExecuteFetch() had returned because we cancelled it,
   153  		// then executeErr would be an error like "MySQL has gone away".
   154  		if executeErr == nil {
   155  			return qr, executeErr
   156  		}
   157  		return nil, ctx.Err()
   158  	}
   159  }
   160  
   161  // killConnection issues a MySQL KILL command for the given connection ID.
   162  func (mysqld *Mysqld) killConnection(connID int64) error {
   163  	// There's no other interface that both types of connection implement.
   164  	// We only care about one method anyway.
   165  	var killConn interface {
   166  		ExecuteFetch(query string, maxrows int, wantfields bool) (*sqltypes.Result, error)
   167  	}
   168  
   169  	// Get another connection with which to kill.
   170  	// Use background context because the caller's context is likely expired,
   171  	// which is the reason we're being asked to kill the connection.
   172  	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
   173  	defer cancel()
   174  	if poolConn, connErr := getPoolReconnect(ctx, mysqld.dbaPool); connErr == nil {
   175  		// We got a pool connection.
   176  		defer poolConn.Recycle()
   177  		killConn = poolConn
   178  	} else {
   179  		// We couldn't get a connection from the pool.
   180  		// It might be because the connection pool is exhausted,
   181  		// because some connections need to be killed!
   182  		// Try to open a new connection without the pool.
   183  		conn, connErr := mysqld.GetDbaConnection(ctx)
   184  		if connErr != nil {
   185  			return connErr
   186  		}
   187  		defer conn.Close()
   188  		killConn = conn
   189  	}
   190  
   191  	_, err := killConn.ExecuteFetch(fmt.Sprintf("kill %d", connID), 10000, false)
   192  	return err
   193  }
   194  
   195  // fetchVariables returns a map from MySQL variable names to variable value
   196  // for variables that match the given pattern.
   197  func (mysqld *Mysqld) fetchVariables(ctx context.Context, pattern string) (map[string]string, error) {
   198  	query := fmt.Sprintf("SHOW VARIABLES LIKE '%s'", pattern)
   199  	qr, err := mysqld.FetchSuperQuery(ctx, query)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	if len(qr.Fields) != 2 {
   204  		return nil, fmt.Errorf("query %#v returned %d columns, expected 2", query, len(qr.Fields))
   205  	}
   206  	varMap := make(map[string]string, len(qr.Rows))
   207  	for _, row := range qr.Rows {
   208  		varMap[row[0].ToString()] = row[1].ToString()
   209  	}
   210  	return varMap, nil
   211  }
   212  
   213  // fetchStatuses returns a map from MySQL status names to status value
   214  // for variables that match the given pattern.
   215  func (mysqld *Mysqld) fetchStatuses(ctx context.Context, pattern string) (map[string]string, error) {
   216  	query := fmt.Sprintf("SHOW STATUS LIKE '%s'", pattern)
   217  	qr, err := mysqld.FetchSuperQuery(ctx, query)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	if len(qr.Fields) != 2 {
   222  		return nil, fmt.Errorf("query %#v returned %d columns, expected 2", query, len(qr.Fields))
   223  	}
   224  	varMap := make(map[string]string, len(qr.Rows))
   225  	for _, row := range qr.Rows {
   226  		varMap[row[0].ToString()] = row[1].ToString()
   227  	}
   228  	return varMap, nil
   229  }
   230  
   231  const (
   232  	masterPasswordStart = "  MASTER_PASSWORD = '"
   233  	masterPasswordEnd   = "',\n"
   234  	passwordStart       = " PASSWORD = '"
   235  	passwordEnd         = "'"
   236  )
   237  
   238  func redactPassword(input string) string {
   239  	i := strings.Index(input, masterPasswordStart)
   240  	// We have primary password in the query, try to redact it
   241  	if i != -1 {
   242  		j := strings.Index(input[i+len(masterPasswordStart):], masterPasswordEnd)
   243  		if j == -1 {
   244  			return input
   245  		}
   246  		input = input[:i+len(masterPasswordStart)] + strings.Repeat("*", 4) + input[i+len(masterPasswordStart)+j:]
   247  	}
   248  	// We also check if we have any password keyword in the query
   249  	i = strings.Index(input, passwordStart)
   250  	if i == -1 {
   251  		return input
   252  	}
   253  	j := strings.Index(input[i+len(passwordStart):], passwordEnd)
   254  	if j == -1 {
   255  		return input
   256  	}
   257  	return input[:i+len(passwordStart)] + strings.Repeat("*", 4) + input[i+len(passwordStart)+j:]
   258  }