github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/conn_executor_savepoints.go (about)

     1  // Copyright 2020 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package sql
    12  
    13  import (
    14  	"context"
    15  	"strings"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/clusterversion"
    18  	"github.com/cockroachdb/cockroach/pkg/kv"
    19  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    20  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
    21  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
    22  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/types"
    25  	"github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented"
    26  	"github.com/cockroachdb/cockroach/pkg/util/fsm"
    27  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    28  	"github.com/cockroachdb/errors"
    29  )
    30  
    31  // commitOnReleaseSavepointName is the name of the savepoint with special
    32  // release semantics: releasing this savepoint commits the underlying KV txn.
    33  // This special savepoint is used to catch deferred serializability violations
    34  // and is part of the client-directed transaction retries protocol.
    35  const commitOnReleaseSavepointName = "cockroach_restart"
    36  
    37  // execSavepointInOpenState runs a SAVEPOINT statement inside an open
    38  // txn.
    39  func (ex *connExecutor) execSavepointInOpenState(
    40  	ctx context.Context, s *tree.Savepoint, res RestrictedCommandResult,
    41  ) (fsm.Event, fsm.EventPayload, error) {
    42  	savepoints := &ex.extraTxnState.savepoints
    43  	// Sanity check for "SAVEPOINT cockroach_restart".
    44  	commitOnRelease := ex.isCommitOnReleaseSavepoint(s.Name)
    45  	if commitOnRelease {
    46  		// Validate the special savepoint cockroach_restart. It cannot be nested
    47  		// because it has special release semantics.
    48  		active := ex.state.mu.txn.Active()
    49  		l := len(*savepoints)
    50  		// If we've already declared this savepoint, but we haven't done anything
    51  		// with the KV txn yet (or, more importantly, we haven't done an anything
    52  		// with the KV txn since we've rolled back to it), treat the recreation of
    53  		// the savepoint as a no-op instead of erroring out because this savepoint
    54  		// cannot be nested (even within itself).
    55  		// This serves to support the following pattern:
    56  		// SAVEPOINT cockroach_restart
    57  		// <foo> -> serializability failure
    58  		// ROLLBACK TO SAVEPOINT cockroach_restart
    59  		// SAVEPOINT cockroach_restart
    60  		//
    61  		// Some of our examples use this pattern, issuing the SAVEPOINT cockroach_restart
    62  		// inside the retry loop.
    63  		//
    64  		// Of course, this means that the following doesn't work:
    65  		// SAVEPOINT cockroach_restart
    66  		// SAVEPOINT cockroach_restart
    67  		// RELEASE SAVEPOINT cockroach_restart
    68  		// ROLLBACK TO SAVEPOINT cockroach_restart  -> the savepoint no longer exists here
    69  		//
    70  		// Although it would work for any other savepoint but cockroach_restart. But
    71  		// that's natural given the release semantics.
    72  		if l == 1 && (*savepoints)[0].commitOnRelease && !active {
    73  			return nil, nil, nil
    74  		}
    75  
    76  		err := func() error {
    77  			if !savepoints.empty() {
    78  				return pgerror.Newf(pgcode.Syntax,
    79  					"SAVEPOINT \"%s\" cannot be nested",
    80  					tree.ErrNameString(commitOnReleaseSavepointName))
    81  			}
    82  			// We want to disallow restart SAVEPOINTs to be issued after a KV
    83  			// transaction has started running. It is desirable to allow metadata
    84  			// queries against vtables to proceed before starting a SAVEPOINT for better
    85  			// ORM compatibility.
    86  			// See also https://github.com/cockroachdb/cockroach/issues/15012.
    87  			if ex.state.mu.txn.Active() {
    88  				return pgerror.Newf(pgcode.Syntax,
    89  					"SAVEPOINT \"%s\" needs to be the first statement in a transaction",
    90  					tree.ErrNameString(commitOnReleaseSavepointName))
    91  			}
    92  			return nil
    93  		}()
    94  		if err != nil {
    95  			ev, payload := ex.makeErrEvent(err, s)
    96  			return ev, payload, nil
    97  		}
    98  	}
    99  
   100  	// We don't support savepoints in mixed-version clusters.
   101  	if !ex.server.cfg.Settings.Version.IsActive(ctx, clusterversion.VersionSavepoints) && !commitOnRelease {
   102  		err := errors.New("savepoints cannot be used until the version upgrade is finalized")
   103  		ev, payload := ex.makeErrEvent(err, s)
   104  		return ev, payload, nil
   105  	}
   106  
   107  	token, err := ex.state.mu.txn.CreateSavepoint(ctx)
   108  	if err != nil {
   109  		ev, payload := ex.makeErrEvent(err, s)
   110  		return ev, payload, nil
   111  	}
   112  
   113  	sp := savepoint{
   114  		name:            s.Name,
   115  		commitOnRelease: commitOnRelease,
   116  		kvToken:         token,
   117  		numDDL:          ex.extraTxnState.numDDL,
   118  	}
   119  	savepoints.push(sp)
   120  
   121  	return nil, nil, nil
   122  }
   123  
   124  // execRelease runs a RELEASE SAVEPOINT statement inside an open txn.
   125  func (ex *connExecutor) execRelease(
   126  	ctx context.Context, s *tree.ReleaseSavepoint, res RestrictedCommandResult,
   127  ) (fsm.Event, fsm.EventPayload) {
   128  	env := &ex.extraTxnState.savepoints
   129  	entry, idx := env.find(s.Savepoint)
   130  	if entry == nil {
   131  		ev, payload := ex.makeErrEvent(
   132  			pgerror.Newf(pgcode.InvalidSavepointSpecification,
   133  				"savepoint \"%s\" does not exist", &s.Savepoint), s)
   134  		return ev, payload
   135  	}
   136  
   137  	// Discard our savepoint and all further ones. Depending on what happens with
   138  	// the release below, we might add this savepoint back.
   139  	env.popToIdx(idx - 1)
   140  
   141  	if entry.commitOnRelease {
   142  		res.ResetStmtType((*tree.CommitTransaction)(nil))
   143  		err := ex.commitSQLTransactionInternal(ctx, s)
   144  		if err == nil {
   145  			return eventTxnReleased{}, nil
   146  		}
   147  		// Committing the transaction failed. We'll go to state RestartWait if
   148  		// it's a retriable error, or to state RollbackWait otherwise.
   149  		if errIsRetriable(err) {
   150  			// Add the savepoint back. We want to allow a ROLLBACK TO SAVEPOINT
   151  			// cockroach_restart (that's the whole point of commitOnRelease).
   152  			env.push(*entry)
   153  
   154  			rc, canAutoRetry := ex.getRewindTxnCapability()
   155  			ev := eventRetriableErr{
   156  				IsCommit:     fsm.FromBool(isCommit(s)),
   157  				CanAutoRetry: fsm.FromBool(canAutoRetry),
   158  			}
   159  			payload := eventRetriableErrPayload{err: err, rewCap: rc}
   160  			return ev, payload
   161  		}
   162  
   163  		// Non-retriable error. The transaction might have committed (i.e. the
   164  		// error might be ambiguous). We can't allow a ROLLBACK TO SAVEPOINT to
   165  		// recover the transaction, so we're not adding the savepoint back.
   166  		ex.rollbackSQLTransaction(ctx)
   167  		ev := eventNonRetriableErr{IsCommit: fsm.FromBool(false)}
   168  		payload := eventNonRetriableErrPayload{err: err}
   169  		return ev, payload
   170  	}
   171  
   172  	if err := ex.state.mu.txn.ReleaseSavepoint(ctx, entry.kvToken); err != nil {
   173  		ev, payload := ex.makeErrEvent(err, s)
   174  		return ev, payload
   175  	}
   176  
   177  	return nil, nil
   178  }
   179  
   180  // execRollbackToSavepointInOpenState runs a ROLLBACK TO SAVEPOINT
   181  // statement inside an open txn.
   182  func (ex *connExecutor) execRollbackToSavepointInOpenState(
   183  	ctx context.Context, s *tree.RollbackToSavepoint, res RestrictedCommandResult,
   184  ) (fsm.Event, fsm.EventPayload) {
   185  	entry, idx := ex.extraTxnState.savepoints.find(s.Savepoint)
   186  	if entry == nil {
   187  		ev, payload := ex.makeErrEvent(pgerror.Newf(pgcode.InvalidSavepointSpecification,
   188  			"savepoint \"%s\" does not exist", &s.Savepoint), s)
   189  		return ev, payload
   190  	}
   191  
   192  	if ev, payload, ok := ex.checkRollbackValidity(ctx, s, entry); !ok {
   193  		return ev, payload
   194  	}
   195  
   196  	// Special case for mixed-cluster versions, where regular savepoints
   197  	// are not yet enabled but we still support cockroach_restart. In
   198  	// that case, we can't process ROLLBACK TO SAVEPOINT
   199  	// cockroach_restart using ignored seqnum lists so we need to
   200  	// restart the txn the "old way".
   201  	// TODO(knz): Remove this check in v20.2 and only keep the 'else'
   202  	// clause, which is the generic rollback code.
   203  	if entry.kvToken.Initial() &&
   204  		!ex.server.cfg.Settings.Version.IsActive(ctx, clusterversion.VersionSavepoints) {
   205  		// Bump the epoch manually.
   206  		ex.state.mu.txn.ManualRestart(ctx, hlc.Timestamp{})
   207  	} else {
   208  		if err := ex.state.mu.txn.RollbackToSavepoint(ctx, entry.kvToken); err != nil {
   209  			ev, payload := ex.makeErrEvent(err, s)
   210  			return ev, payload
   211  		}
   212  	}
   213  
   214  	ex.extraTxnState.savepoints.popToIdx(idx)
   215  
   216  	if entry.kvToken.Initial() {
   217  		return eventTxnRestart{}, nil
   218  	}
   219  	// No event is necessary; there's nothing for the state machine to do.
   220  	return nil, nil
   221  }
   222  
   223  // checkRollbackValidity verifies that a ROLLBACK TO SAVEPOINT
   224  // statement should be allowed in the current txn state.
   225  // It returns ok == false if the operation should be prevented
   226  // from proceeding, in which case it also populates the event
   227  // and payload with a suitable user error.
   228  func (ex *connExecutor) checkRollbackValidity(
   229  	ctx context.Context, s *tree.RollbackToSavepoint, entry *savepoint,
   230  ) (ev fsm.Event, payload fsm.EventPayload, ok bool) {
   231  	if ex.extraTxnState.numDDL <= entry.numDDL {
   232  		// No DDL; all the checks below only care about txns containing
   233  		// DDL, so we don't have anything else to do here.
   234  		return ev, payload, true
   235  	}
   236  
   237  	if !entry.kvToken.Initial() {
   238  		// We don't yet support rolling back a regular savepoint over
   239  		// DDL. Instead of creating an inconsistent txn or schema state,
   240  		// prefer to tell the users we don't know how to proceed
   241  		// yet. Initial savepoints are a special case - we can always
   242  		// rollback to them because we can reset all the schema change
   243  		// state.
   244  		ev, payload = ex.makeErrEvent(unimplemented.NewWithIssueDetail(10735, "rollback-after-ddl",
   245  			"ROLLBACK TO SAVEPOINT not yet supported after DDL statements"), s)
   246  		return ev, payload, false
   247  	}
   248  
   249  	if ex.state.mu.txn.UserPriority() == roachpb.MaxUserPriority {
   250  		// Because we use the same priority (MaxUserPriority) for SET
   251  		// TRANSACTION PRIORITY HIGH and lease acquisitions, we'd get a
   252  		// deadlock if we let DDL proceed at high priority.
   253  		// See https://github.com/cockroachdb/cockroach/issues/46414
   254  		// for details.
   255  		//
   256  		// Note: this check must remain even when regular savepoints are
   257  		// taught to roll back over DDL (that's the other check in
   258  		// execSavepointInOpenState), until #46414 gets solved.
   259  		ev, payload = ex.makeErrEvent(unimplemented.NewWithIssue(46414,
   260  			"cannot use ROLLBACK TO SAVEPOINT in a HIGH PRIORITY transaction containing DDL"), s)
   261  		return ev, payload, false
   262  	}
   263  
   264  	return ev, payload, true
   265  }
   266  
   267  func (ex *connExecutor) execRollbackToSavepointInAbortedState(
   268  	ctx context.Context, s *tree.RollbackToSavepoint,
   269  ) (fsm.Event, fsm.EventPayload) {
   270  	makeErr := func(err error) (fsm.Event, fsm.EventPayload) {
   271  		ev := eventNonRetriableErr{IsCommit: fsm.False}
   272  		payload := eventNonRetriableErrPayload{
   273  			err: err,
   274  		}
   275  		return ev, payload
   276  	}
   277  
   278  	entry, idx := ex.extraTxnState.savepoints.find(s.Savepoint)
   279  	if entry == nil {
   280  		return makeErr(pgerror.Newf(pgcode.InvalidSavepointSpecification,
   281  			"savepoint \"%s\" does not exist", tree.ErrString(&s.Savepoint)))
   282  	}
   283  
   284  	if ev, payload, ok := ex.checkRollbackValidity(ctx, s, entry); !ok {
   285  		return ev, payload
   286  	}
   287  
   288  	ex.extraTxnState.savepoints.popToIdx(idx)
   289  
   290  	// Special case for mixed-cluster versions, where regular savepoints
   291  	// are not yet enabled but we still support cockroach_restart. In
   292  	// that case, we can't process ROLLBACK TO SAVEPOINT
   293  	// cockroach_restart using ignored seqnum lists so we need to
   294  	// restart the txn the "old way".
   295  	// TODO(knz): Remove this check in v20.2 and only keep the 'else'
   296  	// clause, which is the generic rollback code.
   297  	if entry.kvToken.Initial() &&
   298  		!ex.server.cfg.Settings.Version.IsActive(ctx, clusterversion.VersionSavepoints) {
   299  		// Bump the epoch manually.
   300  		ex.state.mu.txn.ManualRestart(ctx, hlc.Timestamp{})
   301  	} else {
   302  		if err := ex.state.mu.txn.RollbackToSavepoint(ctx, entry.kvToken); err != nil {
   303  			return ex.makeErrEvent(err, s)
   304  		}
   305  	}
   306  
   307  	if entry.kvToken.Initial() {
   308  		return eventTxnRestart{}, nil
   309  	}
   310  	return eventSavepointRollback{}, nil
   311  }
   312  
   313  // isCommitOnReleaseSavepoint returns true if the savepoint name implies special
   314  // release semantics: releasing it commits the underlying KV txn.
   315  func (ex *connExecutor) isCommitOnReleaseSavepoint(savepoint tree.Name) bool {
   316  	if ex.sessionData.ForceSavepointRestart {
   317  		// The session setting force_savepoint_restart implies that all
   318  		// uses of the SAVEPOINT statement are targeting restarts.
   319  		return true
   320  	}
   321  	return strings.HasPrefix(string(savepoint), commitOnReleaseSavepointName)
   322  }
   323  
   324  // savepoint represents a SQL savepoint - a snapshot of the current
   325  // transaction's state at a previous point in time.
   326  //
   327  // Savepoints' behavior on RELEASE differs based on commitOnRelease, and their
   328  // behavior on ROLLBACK after retriable errors differs based on
   329  // kvToken.Initial().
   330  type savepoint struct {
   331  	name tree.Name
   332  
   333  	// commitOnRelease is set if the special syntax "SAVEPOINT cockroach_restart"
   334  	// was used. Such a savepoint is special in that a RELEASE actually commits
   335  	// the transaction - giving the client a change to find out about any
   336  	// retriable error and issue another "ROLLBACK TO SAVEPOINT cockroach_restart"
   337  	// afterwards. Regular savepoints (even top-level savepoints) cannot commit
   338  	// the transaction on RELEASE.
   339  	//
   340  	// Only an `initial` savepoint can have this set (see
   341  	// client.SavepointToken.Initial()).
   342  	commitOnRelease bool
   343  
   344  	kvToken kv.SavepointToken
   345  
   346  	// The number of DDL statements that had been executed in the transaction (at
   347  	// the time the savepoint was created). We refuse to roll back a savepoint if
   348  	// more DDL statements were executed since the savepoint's creation.
   349  	// TODO(knz): support partial DDL cancellation in pending txns.
   350  	numDDL int
   351  }
   352  
   353  type savepointStack []savepoint
   354  
   355  func (stack savepointStack) empty() bool { return len(stack) == 0 }
   356  
   357  func (stack *savepointStack) clear() { *stack = (*stack)[:0] }
   358  
   359  func (stack *savepointStack) push(s savepoint) {
   360  	*stack = append(*stack, s)
   361  }
   362  
   363  // find finds the most recent savepoint with the given name.
   364  //
   365  // The returned savepoint can be modified (rolling back modifies the kvToken).
   366  // Callers shouldn't maintain references to the returned savepoint, as
   367  // references can be invalidated by further operations on the savepoints.
   368  func (stack savepointStack) find(sn tree.Name) (*savepoint, int) {
   369  	for i := len(stack) - 1; i >= 0; i-- {
   370  		if stack[i].name == sn {
   371  			return &stack[i], i
   372  		}
   373  	}
   374  	return nil, -1
   375  }
   376  
   377  // popToIdx pops (discards) all the savepoints at higher indexes.
   378  func (stack *savepointStack) popToIdx(idx int) {
   379  	*stack = (*stack)[:idx+1]
   380  }
   381  
   382  func (stack savepointStack) clone() savepointStack {
   383  	if len(stack) == 0 {
   384  		// Avoid allocating a slice.
   385  		return nil
   386  	}
   387  	cpy := make(savepointStack, len(stack))
   388  	copy(cpy, stack)
   389  	return cpy
   390  }
   391  
   392  // runShowSavepointState executes a SHOW SAVEPOINT STATUS statement.
   393  //
   394  // If an error is returned, the connection needs to stop processing queries.
   395  func (ex *connExecutor) runShowSavepointState(
   396  	ctx context.Context, res RestrictedCommandResult,
   397  ) error {
   398  	res.SetColumns(ctx, sqlbase.ResultColumns{
   399  		{Name: "savepoint_name", Typ: types.String},
   400  		{Name: "is_initial_savepoint", Typ: types.Bool},
   401  	})
   402  
   403  	for _, entry := range ex.extraTxnState.savepoints {
   404  		if err := res.AddRow(ctx, tree.Datums{
   405  			tree.NewDString(string(entry.name)),
   406  			tree.MakeDBool(tree.DBool(entry.kvToken.Initial())),
   407  		}); err != nil {
   408  			return err
   409  		}
   410  	}
   411  	return nil
   412  }