vitess.io/vitess@v0.16.2/go/vt/vttablet/tabletserver/tx_pool.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 tabletserver 18 19 import ( 20 "context" 21 "strings" 22 "sync" 23 "time" 24 25 "vitess.io/vitess/go/pools" 26 "vitess.io/vitess/go/timer" 27 "vitess.io/vitess/go/trace" 28 "vitess.io/vitess/go/vt/callerid" 29 "vitess.io/vitess/go/vt/dbconfigs" 30 "vitess.io/vitess/go/vt/log" 31 "vitess.io/vitess/go/vt/servenv" 32 "vitess.io/vitess/go/vt/sqlparser" 33 "vitess.io/vitess/go/vt/vterrors" 34 "vitess.io/vitess/go/vt/vttablet/tabletserver/tabletenv" 35 "vitess.io/vitess/go/vt/vttablet/tabletserver/tx" 36 "vitess.io/vitess/go/vt/vttablet/tabletserver/txlimiter" 37 38 querypb "vitess.io/vitess/go/vt/proto/query" 39 vtrpcpb "vitess.io/vitess/go/vt/proto/vtrpc" 40 ) 41 42 const ( 43 txLogInterval = 1 * time.Minute 44 beginWithCSRO = "start transaction with consistent snapshot, read only" 45 trackGtidQuery = "set session session_track_gtids = START_GTID" 46 ) 47 48 var txIsolations = map[querypb.ExecuteOptions_TransactionIsolation]string{ 49 querypb.ExecuteOptions_DEFAULT: "", 50 querypb.ExecuteOptions_REPEATABLE_READ: "repeatable read", 51 querypb.ExecuteOptions_READ_COMMITTED: "read committed", 52 querypb.ExecuteOptions_READ_UNCOMMITTED: "read uncommitted", 53 querypb.ExecuteOptions_SERIALIZABLE: "serializable", 54 querypb.ExecuteOptions_CONSISTENT_SNAPSHOT_READ_ONLY: "repeatable read", 55 } 56 57 var txAccessMode = map[querypb.ExecuteOptions_TransactionAccessMode]string{ 58 querypb.ExecuteOptions_CONSISTENT_SNAPSHOT: sqlparser.WithConsistentSnapshotStr, 59 querypb.ExecuteOptions_READ_WRITE: sqlparser.ReadWriteStr, 60 querypb.ExecuteOptions_READ_ONLY: sqlparser.ReadOnlyStr, 61 } 62 63 type ( 64 // TxPool does a lot of the transactional operations on StatefulConnections. It does not, with two exceptions, 65 // concern itself with a connections life cycle. The two exceptions are Begin, which creates a new StatefulConnection, 66 // and RollbackAndRelease, which does a Release after doing the rollback. 67 TxPool struct { 68 env tabletenv.Env 69 scp *StatefulConnectionPool 70 ticks *timer.Timer 71 limiter txlimiter.TxLimiter 72 73 logMu sync.Mutex 74 lastLog time.Time 75 txStats *servenv.TimingsWrapper 76 } 77 ) 78 79 // NewTxPool creates a new TxPool. It's not operational until it's Open'd. 80 func NewTxPool(env tabletenv.Env, limiter txlimiter.TxLimiter) *TxPool { 81 config := env.Config() 82 axp := &TxPool{ 83 env: env, 84 scp: NewStatefulConnPool(env), 85 ticks: timer.NewTimer(txKillerTimeoutInterval(config)), 86 limiter: limiter, 87 txStats: env.Exporter().NewTimings("Transactions", "Transaction stats", "operation"), 88 } 89 // Careful: conns also exports name+"xxx" vars, 90 // but we know it doesn't export Timeout. 91 env.Exporter().NewGaugeDurationFunc("OlapTransactionTimeout", "OLAP transaction timeout", func() time.Duration { 92 return config.TxTimeoutForWorkload(querypb.ExecuteOptions_OLAP) 93 }) 94 env.Exporter().NewGaugeDurationFunc("TransactionTimeout", "Transaction timeout", func() time.Duration { 95 return config.TxTimeoutForWorkload(querypb.ExecuteOptions_OLTP) 96 }) 97 return axp 98 } 99 100 // Open makes the TxPool operational. This also starts the transaction killer 101 // that will kill long-running transactions. 102 func (tp *TxPool) Open(appParams, dbaParams, appDebugParams dbconfigs.Connector) { 103 tp.scp.Open(appParams, dbaParams, appDebugParams) 104 if tp.ticks.Interval() > 0 { 105 tp.ticks.Start(func() { tp.transactionKiller() }) 106 } 107 } 108 109 // Close closes the TxPool. A closed pool can be reopened. 110 func (tp *TxPool) Close() { 111 tp.ticks.Stop() 112 tp.scp.Close() 113 } 114 115 // AdjustLastID adjusts the last transaction id to be at least 116 // as large as the input value. This will ensure that there are 117 // no dtid collisions with future transactions. 118 func (tp *TxPool) AdjustLastID(id int64) { 119 tp.scp.AdjustLastID(id) 120 } 121 122 // Shutdown immediately rolls back all transactions that are not in use. 123 // In-use connections will be closed when they are unlocked (not in use). 124 func (tp *TxPool) Shutdown(ctx context.Context) { 125 for _, v := range tp.scp.ShutdownAll() { 126 tp.RollbackAndRelease(ctx, v) 127 } 128 } 129 130 func (tp *TxPool) transactionKiller() { 131 defer tp.env.LogError() 132 for _, conn := range tp.scp.GetElapsedTimeout(vterrors.TxKillerRollback) { 133 log.Warningf("killing transaction (exceeded timeout: %v): %s", conn.timeout, conn.String(tp.env.Config().SanitizeLogMessages)) 134 switch { 135 case conn.IsTainted(): 136 conn.Close() 137 tp.env.Stats().KillCounters.Add("ReservedConnection", 1) 138 case conn.IsInTransaction(): 139 _, err := conn.Exec(context.Background(), "rollback", 1, false) 140 if err != nil { 141 conn.Close() 142 } 143 tp.env.Stats().KillCounters.Add("Transactions", 1) 144 } 145 // For logging, as transaction is killed as the connection is closed. 146 if conn.IsTainted() && conn.IsInTransaction() { 147 tp.env.Stats().KillCounters.Add("Transactions", 1) 148 } 149 if conn.IsInTransaction() { 150 tp.txComplete(conn, tx.TxKill) 151 } 152 conn.Releasef("exceeded timeout: %v", conn.timeout) 153 } 154 } 155 156 // WaitForEmpty waits until all active transactions are completed. 157 func (tp *TxPool) WaitForEmpty() { 158 tp.scp.WaitForEmpty() 159 } 160 161 // NewTxProps creates a new TxProperties struct 162 func (tp *TxPool) NewTxProps(immediateCaller *querypb.VTGateCallerID, effectiveCaller *vtrpcpb.CallerID, autocommit bool) *tx.Properties { 163 return &tx.Properties{ 164 StartTime: time.Now(), 165 EffectiveCaller: effectiveCaller, 166 ImmediateCaller: immediateCaller, 167 Autocommit: autocommit, 168 Stats: tp.txStats, 169 } 170 } 171 172 // GetAndLock fetches the connection associated to the connID and blocks it from concurrent use 173 // You must call Unlock on TxConnection once done. 174 func (tp *TxPool) GetAndLock(connID tx.ConnID, reason string) (*StatefulConnection, error) { 175 conn, err := tp.scp.GetAndLock(connID, reason) 176 if err != nil { 177 return nil, vterrors.Errorf(vtrpcpb.Code_ABORTED, "transaction %d: %v", connID, err) 178 } 179 return conn, nil 180 } 181 182 // Commit commits the transaction on the connection. 183 func (tp *TxPool) Commit(ctx context.Context, txConn *StatefulConnection) (string, error) { 184 if !txConn.IsInTransaction() { 185 return "", vterrors.New(vtrpcpb.Code_INTERNAL, "not in a transaction") 186 } 187 span, ctx := trace.NewSpan(ctx, "TxPool.Commit") 188 defer span.Finish() 189 defer tp.txComplete(txConn, tx.TxCommit) 190 if txConn.TxProperties().Autocommit { 191 return "", nil 192 } 193 194 if _, err := txConn.Exec(ctx, "commit", 1, false); err != nil { 195 txConn.Close() 196 return "", err 197 } 198 return "commit", nil 199 } 200 201 // RollbackAndRelease rolls back the transaction on the specified connection, and releases the connection when done 202 func (tp *TxPool) RollbackAndRelease(ctx context.Context, txConn *StatefulConnection) { 203 defer txConn.Release(tx.TxRollback) 204 rollbackError := tp.Rollback(ctx, txConn) 205 if rollbackError != nil { 206 log.Errorf("tried to rollback, but failed with: %v", rollbackError.Error()) 207 } 208 } 209 210 // Rollback rolls back the transaction on the specified connection. 211 func (tp *TxPool) Rollback(ctx context.Context, txConn *StatefulConnection) error { 212 span, ctx := trace.NewSpan(ctx, "TxPool.Rollback") 213 defer span.Finish() 214 if txConn.IsClosed() || !txConn.IsInTransaction() { 215 return nil 216 } 217 if txConn.TxProperties().Autocommit { 218 tp.txComplete(txConn, tx.TxCommit) 219 return nil 220 } 221 defer tp.txComplete(txConn, tx.TxRollback) 222 if _, err := txConn.Exec(ctx, "rollback", 1, false); err != nil { 223 txConn.Close() 224 return err 225 } 226 return nil 227 } 228 229 // Begin begins a transaction, and returns the associated connection and 230 // the statements (if any) executed to initiate the transaction. In autocommit 231 // mode the statement will be "". 232 // The connection returned is locked for the callee and its responsibility is to unlock the connection. 233 func (tp *TxPool) Begin(ctx context.Context, options *querypb.ExecuteOptions, readOnly bool, reservedID int64, savepointQueries []string, setting *pools.Setting) (*StatefulConnection, string, string, error) { 234 span, ctx := trace.NewSpan(ctx, "TxPool.Begin") 235 defer span.Finish() 236 237 var conn *StatefulConnection 238 var err error 239 if reservedID != 0 { 240 conn, err = tp.scp.GetAndLock(reservedID, "start transaction on reserve conn") 241 if err != nil { 242 return nil, "", "", vterrors.Errorf(vtrpcpb.Code_ABORTED, "transaction %d: %v", reservedID, err) 243 } 244 // Update conn timeout. 245 timeout := tp.env.Config().TxTimeoutForWorkload(options.GetWorkload()) 246 conn.SetTimeout(timeout) 247 } else { 248 immediateCaller := callerid.ImmediateCallerIDFromContext(ctx) 249 effectiveCaller := callerid.EffectiveCallerIDFromContext(ctx) 250 if !tp.limiter.Get(immediateCaller, effectiveCaller) { 251 return nil, "", "", vterrors.Errorf(vtrpcpb.Code_RESOURCE_EXHAUSTED, "per-user transaction pool connection limit exceeded") 252 } 253 conn, err = tp.createConn(ctx, options, setting) 254 defer func() { 255 if err != nil { 256 // The transaction limiter frees transactions on rollback or commit. If we fail to create the transaction, 257 // release immediately since there will be no rollback or commit. 258 tp.limiter.Release(immediateCaller, effectiveCaller) 259 } 260 }() 261 } 262 if err != nil { 263 return nil, "", "", err 264 } 265 sql, sessionStateChanges, err := tp.begin(ctx, options, readOnly, conn, savepointQueries) 266 if err != nil { 267 conn.Close() 268 conn.Release(tx.ConnInitFail) 269 return nil, "", "", err 270 } 271 return conn, sql, sessionStateChanges, nil 272 } 273 274 func (tp *TxPool) begin(ctx context.Context, options *querypb.ExecuteOptions, readOnly bool, conn *StatefulConnection, savepointQueries []string) (string, string, error) { 275 immediateCaller := callerid.ImmediateCallerIDFromContext(ctx) 276 effectiveCaller := callerid.EffectiveCallerIDFromContext(ctx) 277 beginQueries, autocommit, sessionStateChanges, err := createTransaction(ctx, options, conn, readOnly, savepointQueries) 278 if err != nil { 279 return "", "", err 280 } 281 282 conn.txProps = tp.NewTxProps(immediateCaller, effectiveCaller, autocommit) 283 284 return beginQueries, sessionStateChanges, nil 285 } 286 287 func (tp *TxPool) createConn(ctx context.Context, options *querypb.ExecuteOptions, setting *pools.Setting) (*StatefulConnection, error) { 288 conn, err := tp.scp.NewConn(ctx, options, setting) 289 if err != nil { 290 errCode := vterrors.Code(err) 291 switch err { 292 case pools.ErrCtxTimeout: 293 tp.LogActive() 294 err = vterrors.Errorf(errCode, "transaction pool aborting request due to already expired context") 295 case pools.ErrTimeout: 296 tp.LogActive() 297 err = vterrors.Errorf(errCode, "transaction pool connection limit exceeded") 298 } 299 return nil, err 300 } 301 return conn, nil 302 } 303 304 func createTransaction( 305 ctx context.Context, 306 options *querypb.ExecuteOptions, 307 conn *StatefulConnection, 308 readOnly bool, 309 savepointQueries []string, 310 ) (beginQueries string, autocommitTransaction bool, sessionStateChanges string, err error) { 311 switch options.GetTransactionIsolation() { 312 case querypb.ExecuteOptions_CONSISTENT_SNAPSHOT_READ_ONLY: 313 beginQueries, sessionStateChanges, err = handleConsistentSnapshotCase(ctx, conn) 314 if err != nil { 315 return "", false, "", err 316 } 317 case querypb.ExecuteOptions_AUTOCOMMIT: 318 autocommitTransaction = true 319 case querypb.ExecuteOptions_REPEATABLE_READ, querypb.ExecuteOptions_READ_COMMITTED, querypb.ExecuteOptions_READ_UNCOMMITTED, 320 querypb.ExecuteOptions_SERIALIZABLE, querypb.ExecuteOptions_DEFAULT: 321 isolationLevel := txIsolations[options.GetTransactionIsolation()] 322 var execSQL string 323 if isolationLevel != "" { 324 execSQL, err = setIsolationLevel(ctx, conn, isolationLevel) 325 if err != nil { 326 return 327 } 328 beginQueries += execSQL 329 } 330 331 var beginSQL string 332 beginSQL, err = createStartTxStmt(options, readOnly) 333 if err != nil { 334 return "", false, "", err 335 } 336 337 execSQL, sessionStateChanges, err = startTransaction(ctx, conn, beginSQL) 338 if err != nil { 339 return "", false, "", err 340 } 341 342 // Add the begin statement to the list of queries. 343 beginQueries += execSQL 344 default: 345 return "", false, "", vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] don't know how to open a transaction of this type: %v", options.GetTransactionIsolation()) 346 } 347 348 for _, savepoint := range savepointQueries { 349 if _, err = conn.Exec(ctx, savepoint, 1, false); err != nil { 350 return "", false, "", err 351 } 352 } 353 return 354 } 355 356 // createStartTxStmt - this method return the start transaction statement based on the TransactionAccessMode in options 357 // and the readOnly flag passed in. 358 // When readOnly is true, ReadWrite option should not have been passed, that will result in an error. 359 // If no option is passed, the default on the connection will be used by just execution "begin" statement. 360 func createStartTxStmt(options *querypb.ExecuteOptions, readOnly bool) (string, error) { 361 // default statement. 362 beginSQL := "begin" 363 364 // generate the access mode string 365 var modesStr strings.Builder 366 // to know if read only is already added to modeStr 367 // so that explicit addition of read only is not required in case of readOnly parameter is true. 368 var readOnlyAdded bool 369 for idx, accessMode := range options.GetTransactionAccessMode() { 370 txMode, ok := txAccessMode[accessMode] 371 if !ok { 372 return "", vterrors.Errorf(vtrpcpb.Code_INTERNAL, "[BUG] transaction access mode not known of this type: %v", accessMode) 373 } 374 if readOnly && accessMode == querypb.ExecuteOptions_READ_WRITE { 375 return "", vterrors.Errorf(vtrpcpb.Code_FAILED_PRECONDITION, "cannot start read write transaction on a read only tablet") 376 } 377 if accessMode == querypb.ExecuteOptions_READ_ONLY { 378 readOnlyAdded = true 379 } 380 if idx == 0 { 381 modesStr.WriteString(txMode) 382 continue 383 } 384 modesStr.WriteString(", " + txMode) 385 } 386 387 if readOnly && !readOnlyAdded { 388 if modesStr.Len() != 0 { 389 modesStr.WriteString(", read only") 390 } else { 391 modesStr.WriteString("read only") 392 } 393 } 394 if modesStr.Len() != 0 { 395 beginSQL = "start transaction " + modesStr.String() 396 } 397 return beginSQL, nil 398 } 399 400 func handleConsistentSnapshotCase(ctx context.Context, conn *StatefulConnection) (beginSQL string, sessionStateChanges string, err error) { 401 _, err = conn.execWithRetry(ctx, trackGtidQuery, 1, false) 402 // We allow this to fail since this is a custom MySQL extension, but we return 403 // then if this query was executed or not. 404 // 405 // Callers also can know because the sessionStateChanges will be empty for a snapshot 406 // transaction and get GTID information in another (less efficient) way. 407 if err == nil { 408 beginSQL = trackGtidQuery + "; " 409 } 410 411 isolationLevel := txIsolations[querypb.ExecuteOptions_CONSISTENT_SNAPSHOT_READ_ONLY] 412 413 execSQL, err := setIsolationLevel(ctx, conn, isolationLevel) 414 if err != nil { 415 return 416 } 417 beginSQL += execSQL 418 419 execSQL, sessionStateChanges, err = startTransaction(ctx, conn, beginWithCSRO) 420 if err != nil { 421 return 422 } 423 beginSQL += execSQL 424 return 425 } 426 427 func startTransaction(ctx context.Context, conn *StatefulConnection, transaction string) (string, string, error) { 428 sessionStateChanges, err := conn.execWithRetry(ctx, transaction, 1, false) 429 if err != nil { 430 return "", "", err 431 } 432 return transaction, sessionStateChanges, nil 433 } 434 435 func setIsolationLevel(ctx context.Context, conn *StatefulConnection, level string) (string, error) { 436 txQuery := "set transaction isolation level " + level 437 if _, err := conn.execWithRetry(ctx, txQuery, 1, false); err != nil { 438 return "", err 439 } 440 return txQuery + "; ", nil 441 } 442 443 // LogActive causes all existing transactions to be logged when they complete. 444 // The logging is throttled to no more than once every txLogInterval. 445 func (tp *TxPool) LogActive() { 446 tp.logMu.Lock() 447 defer tp.logMu.Unlock() 448 if time.Since(tp.lastLog) < txLogInterval { 449 return 450 } 451 tp.lastLog = time.Now() 452 tp.scp.ForAllTxProperties(func(props *tx.Properties) { 453 props.LogToFile = true 454 }) 455 } 456 457 func (tp *TxPool) txComplete(conn *StatefulConnection, reason tx.ReleaseReason) { 458 conn.LogTransaction(reason) 459 tp.limiter.Release(conn.TxProperties().ImmediateCaller, conn.TxProperties().EffectiveCaller) 460 conn.CleanTxState() 461 } 462 463 func txKillerTimeoutInterval(config *tabletenv.TabletConfig) time.Duration { 464 return smallerTimeout( 465 config.TxTimeoutForWorkload(querypb.ExecuteOptions_OLAP), 466 config.TxTimeoutForWorkload(querypb.ExecuteOptions_OLTP), 467 ) / 10 468 }