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 }