github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvclient/kvcoord/txn_coord_sender_savepoints.go (about)

     1  // Copyright 2019 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 kvcoord
    12  
    13  import (
    14  	"context"
    15  
    16  	"github.com/cockroachdb/cockroach/pkg/kv"
    17  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    18  	"github.com/cockroachdb/cockroach/pkg/storage/enginepb"
    19  	"github.com/cockroachdb/cockroach/pkg/util/errorutil/unimplemented"
    20  	"github.com/cockroachdb/cockroach/pkg/util/uuid"
    21  	"github.com/cockroachdb/errors"
    22  )
    23  
    24  // savepoint captures the state in the TxnCoordSender necessary to restore that
    25  // state upon a savepoint rollback.
    26  type savepoint struct {
    27  	// active is a snapshot of TxnCoordSender.active.
    28  	active bool
    29  
    30  	// txnID and epoch are set for savepoints with the active field set.
    31  	// txnID and epoch are used to disallow rollbacks past transaction restarts.
    32  	// Savepoints without the active field set are allowed to be used to rollback
    33  	// past transaction restarts too, because it's trivial to rollback to the
    34  	// beginning of the transaction.
    35  	txnID uuid.UUID
    36  	epoch enginepb.TxnEpoch
    37  
    38  	// seqNum represents the write seq num at the time the savepoint was created.
    39  	// On rollback, it configures the txn to ignore all seqnums from this value
    40  	// until the most recent seqnum.
    41  	seqNum enginepb.TxnSeq
    42  
    43  	// txnSpanRefresher fields.
    44  	refreshSpans   []roachpb.Span
    45  	refreshInvalid bool
    46  }
    47  
    48  var _ kv.SavepointToken = (*savepoint)(nil)
    49  
    50  // statically allocated savepoint marking the beginning of a transaction. Used
    51  // to avoid allocations for such savepoints.
    52  var initialSavepoint = savepoint{}
    53  
    54  // Initial implements the client.SavepointToken interface.
    55  func (s *savepoint) Initial() bool {
    56  	return !s.active
    57  }
    58  
    59  // CreateSavepoint is part of the client.TxnSender interface.
    60  func (tc *TxnCoordSender) CreateSavepoint(ctx context.Context) (kv.SavepointToken, error) {
    61  	if tc.typ != kv.RootTxn {
    62  		return nil, errors.AssertionFailedf("cannot get savepoint in non-root txn")
    63  	}
    64  
    65  	tc.mu.Lock()
    66  	defer tc.mu.Unlock()
    67  
    68  	if err := tc.assertNotFinalized(); err != nil {
    69  		return nil, err
    70  	}
    71  
    72  	if tc.mu.txnState != txnPending {
    73  		return nil, ErrSavepointOperationInErrorTxn
    74  	}
    75  
    76  	if !tc.mu.active {
    77  		// Return a preallocated savepoint for the common case of savepoints placed
    78  		// at the beginning of transactions.
    79  		return &initialSavepoint, nil
    80  	}
    81  
    82  	s := &savepoint{
    83  		active: true, // we've handled the not-active case above
    84  		txnID:  tc.mu.txn.ID,
    85  		epoch:  tc.mu.txn.Epoch,
    86  	}
    87  	for _, reqInt := range tc.interceptorStack {
    88  		reqInt.createSavepointLocked(ctx, s)
    89  	}
    90  
    91  	return s, nil
    92  }
    93  
    94  // RollbackToSavepoint is part of the client.TxnSender interface.
    95  func (tc *TxnCoordSender) RollbackToSavepoint(ctx context.Context, s kv.SavepointToken) error {
    96  	if tc.typ != kv.RootTxn {
    97  		return errors.AssertionFailedf("cannot rollback savepoint in non-root txn")
    98  	}
    99  
   100  	tc.mu.Lock()
   101  	defer tc.mu.Unlock()
   102  
   103  	if err := tc.assertNotFinalized(); err != nil {
   104  		return err
   105  	}
   106  
   107  	// We don't allow rollback to savepoint after errors (except after
   108  	// ConditionFailedError which is special-cased elsewhere and doesn't move the
   109  	// txn to the txnError state). In particular, we cannot allow rollbacks to
   110  	// savepoint after ambiguous errors where it's possible for a
   111  	// previously-successfully written intent to have been pushed at a timestamp
   112  	// higher than the coordinator's WriteTimestamp. Doing so runs the risk that
   113  	// we'll commit at the lower timestamp, at which point the respective intent
   114  	// will be discarded. See
   115  	// https://github.com/cockroachdb/cockroach/issues/47587.
   116  	//
   117  	// TODO(andrei): White-list more errors.
   118  	if tc.mu.txnState == txnError {
   119  		return unimplemented.New("rollback_error", "cannot rollback to savepoint after error")
   120  	}
   121  
   122  	sp := s.(*savepoint)
   123  	err := tc.checkSavepointLocked(sp)
   124  	if err != nil {
   125  		if errors.Is(err, errSavepointInvalidAfterTxnRestart) {
   126  			err = roachpb.NewTransactionRetryWithProtoRefreshError(
   127  				"cannot rollback to savepoint after a transaction restart",
   128  				tc.mu.txn.ID,
   129  				// The transaction inside this error doesn't matter.
   130  				roachpb.Transaction{},
   131  			)
   132  		}
   133  		return err
   134  	}
   135  
   136  	// Restore the transaction's state, in case we're rewiding after an error.
   137  	tc.mu.txnState = txnPending
   138  
   139  	tc.mu.active = sp.active
   140  
   141  	for _, reqInt := range tc.interceptorStack {
   142  		reqInt.rollbackToSavepointLocked(ctx, *sp)
   143  	}
   144  
   145  	// If there's been any more writes since the savepoint was created, they'll
   146  	// need to be ignored.
   147  	if sp.seqNum < tc.interceptorAlloc.txnSeqNumAllocator.writeSeq {
   148  		tc.mu.txn.AddIgnoredSeqNumRange(
   149  			enginepb.IgnoredSeqNumRange{
   150  				Start: sp.seqNum + 1, End: tc.interceptorAlloc.txnSeqNumAllocator.writeSeq,
   151  			})
   152  	}
   153  
   154  	return nil
   155  }
   156  
   157  // ReleaseSavepoint is part of the client.TxnSender interface.
   158  func (tc *TxnCoordSender) ReleaseSavepoint(ctx context.Context, s kv.SavepointToken) error {
   159  	if tc.typ != kv.RootTxn {
   160  		return errors.AssertionFailedf("cannot release savepoint in non-root txn")
   161  	}
   162  
   163  	tc.mu.Lock()
   164  	defer tc.mu.Unlock()
   165  
   166  	if tc.mu.txnState != txnPending {
   167  		return ErrSavepointOperationInErrorTxn
   168  	}
   169  
   170  	sp := s.(*savepoint)
   171  	err := tc.checkSavepointLocked(sp)
   172  	if errors.Is(err, errSavepointInvalidAfterTxnRestart) {
   173  		err = roachpb.NewTransactionRetryWithProtoRefreshError(
   174  			"cannot release savepoint after a transaction restart",
   175  			tc.mu.txn.ID,
   176  			// The transaction inside this error doesn't matter.
   177  			roachpb.Transaction{},
   178  		)
   179  	}
   180  	return err
   181  }
   182  
   183  type errSavepointOperationInErrorTxn struct{}
   184  
   185  // ErrSavepointOperationInErrorTxn is reported when CreateSavepoint()
   186  // or ReleaseSavepoint() is called over a txn currently in error.
   187  var ErrSavepointOperationInErrorTxn error = errSavepointOperationInErrorTxn{}
   188  
   189  func (err errSavepointOperationInErrorTxn) Error() string {
   190  	return "cannot create or release savepoint after an error has occurred"
   191  }
   192  
   193  func (tc *TxnCoordSender) assertNotFinalized() error {
   194  	if tc.mu.txnState == txnFinalized {
   195  		return errors.AssertionFailedf("operation invalid for finalized txns")
   196  	}
   197  	return nil
   198  }
   199  
   200  var errSavepointInvalidAfterTxnRestart = errors.New("savepoint invalid after transaction restart")
   201  
   202  // checkSavepointLocked checks whether the provided savepoint is still valid.
   203  // Returns errSavepointInvalidAfterTxnRestart if the savepoint is not an
   204  // "initial" one and the transaction has restarted since the savepoint was
   205  // created.
   206  func (tc *TxnCoordSender) checkSavepointLocked(s *savepoint) error {
   207  	// Only savepoints taken before any activity are allowed to be used after a
   208  	// transaction restart.
   209  	if s.Initial() {
   210  		return nil
   211  	}
   212  	if s.txnID != tc.mu.txn.ID {
   213  		return errSavepointInvalidAfterTxnRestart
   214  	}
   215  	if s.epoch != tc.mu.txn.Epoch {
   216  		return errSavepointInvalidAfterTxnRestart
   217  	}
   218  
   219  	if s.seqNum < 0 || s.seqNum > tc.interceptorAlloc.txnSeqNumAllocator.writeSeq {
   220  		return errors.AssertionFailedf("invalid savepoint: got %d, expected 0-%d",
   221  			s.seqNum, tc.interceptorAlloc.txnSeqNumAllocator.writeSeq)
   222  	}
   223  
   224  	return nil
   225  }