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  }