github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvserver/batcheval/cmd_end_transaction_test.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 batcheval
    12  
    13  import (
    14  	"context"
    15  	"regexp"
    16  	"testing"
    17  
    18  	"github.com/cockroachdb/cockroach/pkg/keys"
    19  	"github.com/cockroachdb/cockroach/pkg/kv/kvserver/abortspan"
    20  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    21  	"github.com/cockroachdb/cockroach/pkg/storage"
    22  	"github.com/cockroachdb/cockroach/pkg/storage/enginepb"
    23  	"github.com/cockroachdb/cockroach/pkg/testutils"
    24  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    25  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    26  	"github.com/stretchr/testify/require"
    27  )
    28  
    29  // TestEndTxnUpdatesTransactionRecord tests EndTxn request across its various
    30  // possible transaction record state transitions and error cases.
    31  func TestEndTxnUpdatesTransactionRecord(t *testing.T) {
    32  	defer leaktest.AfterTest(t)()
    33  
    34  	ctx := context.Background()
    35  	startKey := roachpb.Key("0000")
    36  	endKey := roachpb.Key("9999")
    37  	desc := roachpb.RangeDescriptor{
    38  		RangeID:  99,
    39  		StartKey: roachpb.RKey(startKey),
    40  		EndKey:   roachpb.RKey(endKey),
    41  	}
    42  	as := abortspan.New(desc.RangeID)
    43  
    44  	k, k2 := roachpb.Key("a"), roachpb.Key("b")
    45  	ts, ts2, ts3 := hlc.Timestamp{WallTime: 1}, hlc.Timestamp{WallTime: 2}, hlc.Timestamp{WallTime: 3}
    46  	txn := roachpb.MakeTransaction("test", k, 0, ts, 0)
    47  	writes := []roachpb.SequencedWrite{{Key: k, Sequence: 0}}
    48  	intents := []roachpb.Span{{Key: k2}}
    49  
    50  	headerTxn := txn.Clone()
    51  	pushedHeaderTxn := txn.Clone()
    52  	pushedHeaderTxn.WriteTimestamp.Forward(ts2)
    53  	refreshedHeaderTxn := txn.Clone()
    54  	refreshedHeaderTxn.WriteTimestamp.Forward(ts2)
    55  	refreshedHeaderTxn.ReadTimestamp.Forward(ts2)
    56  	restartedHeaderTxn := txn.Clone()
    57  	restartedHeaderTxn.Restart(-1, 0, ts2)
    58  	restartedAndPushedHeaderTxn := txn.Clone()
    59  	restartedAndPushedHeaderTxn.Restart(-1, 0, ts2)
    60  	restartedAndPushedHeaderTxn.WriteTimestamp.Forward(ts3)
    61  
    62  	pendingRecord := func() *roachpb.TransactionRecord {
    63  		record := txn.AsRecord()
    64  		record.Status = roachpb.PENDING
    65  		return &record
    66  	}()
    67  	stagingRecord := func() *roachpb.TransactionRecord {
    68  		record := txn.AsRecord()
    69  		record.Status = roachpb.STAGING
    70  		record.LockSpans = intents
    71  		record.InFlightWrites = writes
    72  		return &record
    73  	}()
    74  	committedRecord := func() *roachpb.TransactionRecord {
    75  		record := txn.AsRecord()
    76  		record.Status = roachpb.COMMITTED
    77  		record.LockSpans = intents
    78  		return &record
    79  	}()
    80  	abortedRecord := func() *roachpb.TransactionRecord {
    81  		record := txn.AsRecord()
    82  		record.Status = roachpb.ABORTED
    83  		record.LockSpans = intents
    84  		return &record
    85  	}()
    86  
    87  	testCases := []struct {
    88  		name string
    89  		// Replica state.
    90  		existingTxn  *roachpb.TransactionRecord
    91  		canCreateTxn func() (can bool, minTS hlc.Timestamp)
    92  		// Request state.
    93  		headerTxn      *roachpb.Transaction
    94  		commit         bool
    95  		noLockSpans    bool
    96  		inFlightWrites []roachpb.SequencedWrite
    97  		deadline       *hlc.Timestamp
    98  		// Expected result.
    99  		expError string
   100  		expTxn   *roachpb.TransactionRecord
   101  	}{
   102  		{
   103  			// Standard case where a transaction is rolled back when
   104  			// there are intents to clean up.
   105  			name: "record missing, try rollback",
   106  			// Replica state.
   107  			existingTxn:  nil,
   108  			canCreateTxn: nil, // not needed
   109  			// Request state.
   110  			headerTxn: headerTxn,
   111  			commit:    false,
   112  			// Expected result.
   113  			// If the transaction record doesn't exist, a rollback that needs
   114  			// to record remote intents will create it.
   115  			expTxn: abortedRecord,
   116  		},
   117  		{
   118  			// Non-standard case. Mimics a transaction being cleaned up
   119  			// when all intents are on the transaction record's range.
   120  			name: "record missing, try rollback without intents",
   121  			// Replica state.
   122  			existingTxn:  nil,
   123  			canCreateTxn: nil, // not needed
   124  			// Request state.
   125  			headerTxn:   headerTxn,
   126  			commit:      false,
   127  			noLockSpans: true,
   128  			// Expected result.
   129  			// If the transaction record doesn't exist, a rollback that doesn't
   130  			// need to record any remote intents won't create it.
   131  			expTxn: nil,
   132  		},
   133  		{
   134  			// Either a PushTxn(ABORT) request succeeded or this is a replay
   135  			// and the transaction has already been finalized. Either way,
   136  			// the request isn't allowed to create a new transaction record.
   137  			name: "record missing, can't create, try stage",
   138  			// Replica state.
   139  			existingTxn:  nil,
   140  			canCreateTxn: func() (bool, hlc.Timestamp) { return false, hlc.Timestamp{} },
   141  			// Request state.
   142  			headerTxn:      headerTxn,
   143  			commit:         true,
   144  			inFlightWrites: writes,
   145  			// Expected result.
   146  			expError: "TransactionAbortedError(ABORT_REASON_ABORTED_RECORD_FOUND)",
   147  		},
   148  		{
   149  			// Either a PushTxn(ABORT) request succeeded or this is a replay
   150  			// and the transaction has already been finalized. Either way,
   151  			// the request isn't allowed to create a new transaction record.
   152  			name: "record missing, can't create, try commit",
   153  			// Replica state.
   154  			existingTxn:  nil,
   155  			canCreateTxn: func() (bool, hlc.Timestamp) { return false, hlc.Timestamp{} },
   156  			// Request state.
   157  			headerTxn: headerTxn,
   158  			commit:    true,
   159  			// Expected result.
   160  			expError: "TransactionAbortedError(ABORT_REASON_ABORTED_RECORD_FOUND)",
   161  		},
   162  		{
   163  			// Standard case where a transaction record is created during a
   164  			// parallel commit.
   165  			name: "record missing, can create, try stage",
   166  			// Replica state.
   167  			existingTxn:  nil,
   168  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   169  			// Request state.
   170  			headerTxn:      headerTxn,
   171  			commit:         true,
   172  			inFlightWrites: writes,
   173  			// Expected result.
   174  			expTxn: stagingRecord,
   175  		},
   176  		{
   177  			// Standard case where a transaction record is created during a
   178  			// non-parallel commit.
   179  			name: "record missing, can create, try commit",
   180  			// Replica state.
   181  			existingTxn:  nil,
   182  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   183  			// Request state.
   184  			headerTxn: headerTxn,
   185  			commit:    true,
   186  			// Expected result.
   187  			expTxn: committedRecord,
   188  		},
   189  		{
   190  			// Standard case where a transaction record is created during a
   191  			// parallel commit when all writes are still in-flight.
   192  			name: "record missing, can create, try stage without intents",
   193  			// Replica state.
   194  			existingTxn:  nil,
   195  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   196  			// Request state.
   197  			headerTxn:      headerTxn,
   198  			commit:         true,
   199  			noLockSpans:    true,
   200  			inFlightWrites: writes,
   201  			// Expected result.
   202  			expTxn: func() *roachpb.TransactionRecord {
   203  				record := *stagingRecord
   204  				record.LockSpans = nil
   205  				return &record
   206  			}(),
   207  		},
   208  		{
   209  			// Non-standard case where a transaction record is created during a
   210  			// non-parallel commit when there are no intents. Mimics a transaction
   211  			// being committed when all intents are on the transaction record's
   212  			// range.
   213  			name: "record missing, can create, try commit without intents",
   214  			// Replica state.
   215  			existingTxn:  nil,
   216  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   217  			// Request state.
   218  			headerTxn:   headerTxn,
   219  			commit:      true,
   220  			noLockSpans: true,
   221  			// Expected result.
   222  			// If the transaction record doesn't exist, a commit that doesn't
   223  			// need to record any remote intents won't create it.
   224  			expTxn: nil,
   225  		},
   226  		{
   227  			// The transaction's commit timestamp was increased during its
   228  			// lifetime, but it hasn't refreshed up to its new commit timestamp.
   229  			// The stage will be rejected.
   230  			name: "record missing, can create, try stage at pushed timestamp",
   231  			// Replica state.
   232  			existingTxn:  nil,
   233  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   234  			// Request state.
   235  			headerTxn:      pushedHeaderTxn,
   236  			commit:         true,
   237  			inFlightWrites: writes,
   238  			// Expected result.
   239  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   240  		},
   241  		{
   242  			// The transaction's commit timestamp was increased during its
   243  			// lifetime, but it hasn't refreshed up to its new commit timestamp.
   244  			// The commit will be rejected.
   245  			name: "record missing, can create, try commit at pushed timestamp",
   246  			// Replica state.
   247  			existingTxn:  nil,
   248  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   249  			// Request state.
   250  			headerTxn: pushedHeaderTxn,
   251  			commit:    true,
   252  			// Expected result.
   253  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   254  		},
   255  		{
   256  			// The transaction's commit timestamp was increased during its
   257  			// lifetime and it has refreshed up to this timestamp. The stage
   258  			// will succeed.
   259  			name: "record missing, can create, try stage at pushed timestamp after refresh",
   260  			// Replica state.
   261  			existingTxn:  nil,
   262  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   263  			// Request state.
   264  			headerTxn:      refreshedHeaderTxn,
   265  			commit:         true,
   266  			inFlightWrites: writes,
   267  			// Expected result.
   268  			expTxn: func() *roachpb.TransactionRecord {
   269  				record := *stagingRecord
   270  				record.WriteTimestamp.Forward(ts2)
   271  				return &record
   272  			}(),
   273  		},
   274  		{
   275  			// The transaction's commit timestamp was increased during its
   276  			// lifetime and it has refreshed up to this timestamp. The commit
   277  			// will succeed.
   278  			name: "record missing, can create, try commit at pushed timestamp after refresh",
   279  			// Replica state.
   280  			existingTxn:  nil,
   281  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   282  			// Request state.
   283  			headerTxn: refreshedHeaderTxn,
   284  			commit:    true,
   285  			// Expected result.
   286  			expTxn: func() *roachpb.TransactionRecord {
   287  				record := *committedRecord
   288  				record.WriteTimestamp.Forward(ts2)
   289  				return &record
   290  			}(),
   291  		},
   292  		{
   293  			// A PushTxn(TIMESTAMP) request bumped the minimum timestamp that the
   294  			// transaction can be created with. This will trigger a retry error.
   295  			name: "record missing, can create with min timestamp, try stage",
   296  			// Replica state.
   297  			existingTxn:  nil,
   298  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, ts2 },
   299  			// Request state.
   300  			headerTxn:      headerTxn,
   301  			commit:         true,
   302  			inFlightWrites: writes,
   303  			// Expected result.
   304  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   305  		},
   306  		{
   307  			// A PushTxn(TIMESTAMP) request bumped the minimum timestamp that the
   308  			// transaction can be created with. This will trigger a retry error.
   309  			name: "record missing, can create with min timestamp, try commit",
   310  			// Replica state.
   311  			existingTxn:  nil,
   312  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, ts2 },
   313  			// Request state.
   314  			headerTxn: headerTxn,
   315  			commit:    true,
   316  			// Expected result.
   317  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   318  		},
   319  		{
   320  			// A PushTxn(TIMESTAMP) request bumped the minimum timestamp that
   321  			// the transaction can be created with. Luckily, the transaction has
   322  			// already refreshed above this time, so it can avoid a retry error.
   323  			name: "record missing, can create with min timestamp, try stage at pushed timestamp after refresh",
   324  			// Replica state.
   325  			existingTxn:  nil,
   326  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, ts2 },
   327  			// Request state.
   328  			headerTxn:      refreshedHeaderTxn,
   329  			commit:         true,
   330  			inFlightWrites: writes,
   331  			// Expected result.
   332  			expTxn: func() *roachpb.TransactionRecord {
   333  				record := *stagingRecord
   334  				record.WriteTimestamp.Forward(ts2)
   335  				return &record
   336  			}(),
   337  		},
   338  		{
   339  			// A PushTxn(TIMESTAMP) request bumped the minimum timestamp that
   340  			// the transaction can be created with. Luckily, the transaction has
   341  			// already refreshed above this time, so it can avoid a retry error.
   342  			name: "record missing, can create with min timestamp, try commit at pushed timestamp after refresh",
   343  			// Replica state.
   344  			existingTxn:  nil,
   345  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, ts2 },
   346  			// Request state.
   347  			headerTxn: refreshedHeaderTxn,
   348  			commit:    true,
   349  			// Expected result.
   350  			expTxn: func() *roachpb.TransactionRecord {
   351  				record := *committedRecord
   352  				record.WriteTimestamp.Forward(ts2)
   353  				return &record
   354  			}(),
   355  		},
   356  		{
   357  			// The transaction has run into a WriteTooOld error during its
   358  			// lifetime. The stage will be rejected.
   359  			name: "record missing, can create, try stage after write too old",
   360  			// Replica state.
   361  			existingTxn:  nil,
   362  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   363  			// Request state.
   364  			headerTxn: func() *roachpb.Transaction {
   365  				clone := txn.Clone()
   366  				clone.WriteTooOld = true
   367  				return clone
   368  			}(),
   369  			commit:         true,
   370  			inFlightWrites: writes,
   371  			// Expected result.
   372  			expError: "TransactionRetryError: retry txn (RETRY_WRITE_TOO_OLD)",
   373  		},
   374  		{
   375  			// The transaction has run into a WriteTooOld error during its
   376  			// lifetime. The stage will be rejected.
   377  			name: "record missing, can create, try commit after write too old",
   378  			// Replica state.
   379  			existingTxn:  nil,
   380  			canCreateTxn: func() (bool, hlc.Timestamp) { return true, hlc.Timestamp{} },
   381  			// Request state.
   382  			headerTxn: func() *roachpb.Transaction {
   383  				clone := txn.Clone()
   384  				clone.WriteTooOld = true
   385  				return clone
   386  			}(),
   387  			commit: true,
   388  			// Expected result.
   389  			expError: "TransactionRetryError: retry txn (RETRY_WRITE_TOO_OLD)",
   390  		},
   391  		{
   392  			// Standard case where a transaction is rolled back. The record
   393  			// already exists because it has been heartbeated.
   394  			name: "record pending, try rollback",
   395  			// Replica state.
   396  			existingTxn: pendingRecord,
   397  			// Request state.
   398  			headerTxn: headerTxn,
   399  			commit:    false,
   400  			// Expected result.
   401  			expTxn: abortedRecord,
   402  		},
   403  		{
   404  			// Standard case where a transaction record is created during a
   405  			// parallel commit. The record already exists because it has been
   406  			// heartbeated.
   407  			name: "record pending, try stage",
   408  			// Replica state.
   409  			existingTxn: pendingRecord,
   410  			// Request state.
   411  			headerTxn:      headerTxn,
   412  			commit:         true,
   413  			inFlightWrites: writes,
   414  			// Expected result.
   415  			expTxn: stagingRecord,
   416  		},
   417  		{
   418  			// Standard case where a transaction record is created during a
   419  			// non-parallel commit. The record already exists because it has
   420  			// been heartbeated.
   421  			name: "record pending, try commit",
   422  			// Replica state.
   423  			existingTxn: pendingRecord,
   424  			// Request state.
   425  			headerTxn: headerTxn,
   426  			commit:    true,
   427  			// Expected result.
   428  			expTxn: committedRecord,
   429  		},
   430  		{
   431  			// The transaction's commit timestamp was increased during its
   432  			// lifetime, but it hasn't refreshed up to its new commit timestamp.
   433  			// The stage will be rejected.
   434  			name: "record pending, try stage at pushed timestamp",
   435  			// Replica state.
   436  			existingTxn: pendingRecord,
   437  			// Request state.
   438  			headerTxn:      pushedHeaderTxn,
   439  			commit:         true,
   440  			inFlightWrites: writes,
   441  			// Expected result.
   442  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   443  		},
   444  		{
   445  			// The transaction's commit timestamp was increased during its
   446  			// lifetime, but it hasn't refreshed up to its new commit timestamp.
   447  			// The commit will be rejected.
   448  			name: "record pending, try commit at pushed timestamp",
   449  			// Replica state.
   450  			existingTxn: pendingRecord,
   451  			// Request state.
   452  			headerTxn: pushedHeaderTxn,
   453  			commit:    true,
   454  			// Expected result.
   455  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   456  		},
   457  		{
   458  			// The transaction's commit timestamp was increased during its
   459  			// lifetime and it has refreshed up to this timestamp. The stage
   460  			// will succeed.
   461  			name: "record pending, try stage at pushed timestamp after refresh",
   462  			// Replica state.
   463  			existingTxn: pendingRecord,
   464  			// Request state.
   465  			headerTxn:      refreshedHeaderTxn,
   466  			commit:         true,
   467  			inFlightWrites: writes,
   468  			// Expected result.
   469  			expTxn: func() *roachpb.TransactionRecord {
   470  				record := *stagingRecord
   471  				record.WriteTimestamp.Forward(ts2)
   472  				return &record
   473  			}(),
   474  		},
   475  		{
   476  			// The transaction's commit timestamp was increased during its
   477  			// lifetime and it has refreshed up to this timestamp. The commit
   478  			// will succeed.
   479  			name: "record pending, try commit at pushed timestamp after refresh",
   480  			// Replica state.
   481  			existingTxn: pendingRecord,
   482  			// Request state.
   483  			headerTxn: refreshedHeaderTxn,
   484  			commit:    true,
   485  			// Expected result.
   486  			expTxn: func() *roachpb.TransactionRecord {
   487  				record := *committedRecord
   488  				record.WriteTimestamp.Forward(ts2)
   489  				return &record
   490  			}(),
   491  		},
   492  		{
   493  			// The transaction has run into a WriteTooOld error during its
   494  			// lifetime. The stage will be rejected.
   495  			name: "record pending, try stage after write too old",
   496  			// Replica state.
   497  			existingTxn: pendingRecord,
   498  			// Request state.
   499  			headerTxn: func() *roachpb.Transaction {
   500  				clone := txn.Clone()
   501  				clone.WriteTooOld = true
   502  				return clone
   503  			}(),
   504  			commit:         true,
   505  			inFlightWrites: writes,
   506  			// Expected result.
   507  			expError: "TransactionRetryError: retry txn (RETRY_WRITE_TOO_OLD)",
   508  		},
   509  		{
   510  			// The transaction has run into a WriteTooOld error during its
   511  			// lifetime. The stage will be rejected.
   512  			name: "record pending, try commit after write too old",
   513  			// Replica state.
   514  			existingTxn: pendingRecord,
   515  			// Request state.
   516  			headerTxn: func() *roachpb.Transaction {
   517  				clone := txn.Clone()
   518  				clone.WriteTooOld = true
   519  				return clone
   520  			}(),
   521  			commit: true,
   522  			// Expected result.
   523  			expError: "TransactionRetryError: retry txn (RETRY_WRITE_TOO_OLD)",
   524  		},
   525  		{
   526  			// Standard case where a transaction is rolled back after it has
   527  			// written a record at a lower epoch. The existing record is
   528  			// upgraded.
   529  			name: "record pending, try rollback at higher epoch",
   530  			// Replica state.
   531  			existingTxn: pendingRecord,
   532  			// Request state.
   533  			headerTxn: restartedHeaderTxn,
   534  			commit:    false,
   535  			// Expected result.
   536  			expTxn: func() *roachpb.TransactionRecord {
   537  				record := *abortedRecord
   538  				record.Epoch++
   539  				record.WriteTimestamp.Forward(ts2)
   540  				return &record
   541  			}(),
   542  		},
   543  		{
   544  			// Standard case where a transaction record is created during a
   545  			// parallel commit after it has written a record at a lower epoch.
   546  			// The existing record is upgraded.
   547  			name: "record pending, try stage at higher epoch",
   548  			// Replica state.
   549  			existingTxn: pendingRecord,
   550  			// Request state.
   551  			headerTxn:      restartedHeaderTxn,
   552  			commit:         true,
   553  			inFlightWrites: writes,
   554  			// Expected result.
   555  			expTxn: func() *roachpb.TransactionRecord {
   556  				record := *stagingRecord
   557  				record.Epoch++
   558  				record.WriteTimestamp.Forward(ts2)
   559  				return &record
   560  			}(),
   561  		},
   562  		{
   563  			// Standard case where a transaction record is created during a
   564  			// non-parallel commit after it has written a record at a lower
   565  			// epoch. The existing record is upgraded.
   566  			name: "record pending, try commit at higher epoch",
   567  			// Replica state.
   568  			existingTxn: pendingRecord,
   569  			// Request state.
   570  			headerTxn: restartedHeaderTxn,
   571  			commit:    true,
   572  			// Expected result.
   573  			expTxn: func() *roachpb.TransactionRecord {
   574  				record := *committedRecord
   575  				record.Epoch++
   576  				record.WriteTimestamp.Forward(ts2)
   577  				return &record
   578  			}(),
   579  		},
   580  		{
   581  			// The transaction's commit timestamp was increased during the
   582  			// current epoch, but it hasn't refreshed up to its new commit
   583  			// timestamp. The stage will be rejected.
   584  			name: "record pending, try stage at higher epoch and pushed timestamp",
   585  			// Replica state.
   586  			existingTxn: pendingRecord,
   587  			// Request state.
   588  			headerTxn:      restartedAndPushedHeaderTxn,
   589  			commit:         true,
   590  			inFlightWrites: writes,
   591  			// Expected result.
   592  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   593  		},
   594  		{
   595  			// The transaction's commit timestamp was increased during the
   596  			// current epoch, but it hasn't refreshed up to its new commit
   597  			// timestamp. The commit will be rejected.
   598  			name: "record pending, try commit at higher epoch and pushed timestamp",
   599  			// Replica state.
   600  			existingTxn: pendingRecord,
   601  			// Request state.
   602  			headerTxn: restartedAndPushedHeaderTxn,
   603  			commit:    true,
   604  			// Expected result.
   605  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   606  		},
   607  		{
   608  			// Standard case where a transaction is rolled back. The record
   609  			// already exists because of a failed parallel commit attempt.
   610  			name: "record staging, try rollback",
   611  			// Replica state.
   612  			existingTxn: stagingRecord,
   613  			// Request state.
   614  			headerTxn: headerTxn,
   615  			commit:    false,
   616  			// Expected result.
   617  			expTxn: abortedRecord,
   618  		},
   619  		{
   620  			// Standard case where a transaction record is created during a
   621  			// parallel commit. The record already exists because of a failed
   622  			// parallel commit attempt.
   623  			name: "record staging, try re-stage",
   624  			// Replica state.
   625  			existingTxn: stagingRecord,
   626  			// Request state.
   627  			headerTxn:      headerTxn,
   628  			commit:         true,
   629  			inFlightWrites: writes,
   630  			// Expected result.
   631  			expTxn: stagingRecord,
   632  		},
   633  		{
   634  			// Standard case where a transaction record is created during a
   635  			// non-parallel commit. The record already exists because of a
   636  			// failed parallel commit attempt.
   637  			name: "record staging, try commit",
   638  			// Replica state.
   639  			existingTxn: stagingRecord,
   640  			// Request state.
   641  			headerTxn: headerTxn,
   642  			commit:    true,
   643  			// Expected result.
   644  			expTxn: committedRecord,
   645  		},
   646  		{
   647  			// Non-standard case where a transaction record is created during a
   648  			// parallel commit. The record already exists because of a failed
   649  			// parallel commit attempt. The re-stage will fail because of the
   650  			// pushed timestamp.
   651  			name: "record staging, try re-stage at pushed timestamp",
   652  			// Replica state.
   653  			existingTxn: stagingRecord,
   654  			// Request state.
   655  			headerTxn:      pushedHeaderTxn,
   656  			commit:         true,
   657  			inFlightWrites: writes,
   658  			// Expected result.
   659  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   660  		},
   661  		{
   662  			// Non-standard case where a transaction record is created during
   663  			// a non-parallel commit. The record already exists because of a
   664  			// failed parallel commit attempt. The commit will fail because of
   665  			// the pushed timestamp.
   666  			name: "record staging, try commit at pushed timestamp",
   667  			// Replica state.
   668  			existingTxn: stagingRecord,
   669  			// Request state.
   670  			headerTxn: pushedHeaderTxn,
   671  			commit:    true,
   672  			// Expected result.
   673  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   674  		},
   675  		{
   676  			// Non-standard case where a transaction is rolled back. The record
   677  			// already exists because of a failed parallel commit attempt in a
   678  			// prior epoch.
   679  			name: "record staging, try rollback at higher epoch",
   680  			// Replica state.
   681  			existingTxn: stagingRecord,
   682  			// Request state.
   683  			headerTxn: restartedHeaderTxn,
   684  			commit:    false,
   685  			// Expected result.
   686  			expTxn: func() *roachpb.TransactionRecord {
   687  				record := *abortedRecord
   688  				record.Epoch++
   689  				record.WriteTimestamp.Forward(ts2)
   690  				return &record
   691  			}(),
   692  		},
   693  		{
   694  			// Non-standard case where a transaction record is created during a
   695  			// parallel commit. The record already exists because of a failed
   696  			// parallel commit attempt in a prior epoch.
   697  			name: "record staging, try re-stage at higher epoch",
   698  			// Replica state.
   699  			existingTxn: stagingRecord,
   700  			// Request state.
   701  			headerTxn:      restartedHeaderTxn,
   702  			commit:         true,
   703  			inFlightWrites: writes,
   704  			// Expected result.
   705  			expTxn: func() *roachpb.TransactionRecord {
   706  				record := *stagingRecord
   707  				record.Epoch++
   708  				record.WriteTimestamp.Forward(ts2)
   709  				return &record
   710  			}(),
   711  		},
   712  		{
   713  			// Non-standard case where a transaction record is created during
   714  			// a non-parallel commit. The record already exists because of a
   715  			// failed parallel commit attempt in a prior epoch.
   716  			name: "record staging, try commit at higher epoch",
   717  			// Replica state.
   718  			existingTxn: stagingRecord,
   719  			// Request state.
   720  			headerTxn: restartedHeaderTxn,
   721  			commit:    true,
   722  			// Expected result.
   723  			expTxn: func() *roachpb.TransactionRecord {
   724  				record := *committedRecord
   725  				record.Epoch++
   726  				record.WriteTimestamp.Forward(ts2)
   727  				return &record
   728  			}(),
   729  		},
   730  		{
   731  			// Non-standard case where a transaction record is created during a
   732  			// parallel commit. The record already exists because of a failed
   733  			// parallel commit attempt in a prior epoch. The re-stage will fail
   734  			// because of the pushed timestamp.
   735  			name: "record staging, try re-stage at higher epoch and pushed timestamp",
   736  			// Replica state.
   737  			existingTxn: stagingRecord,
   738  			// Request state.
   739  			headerTxn:      restartedAndPushedHeaderTxn,
   740  			commit:         true,
   741  			inFlightWrites: writes,
   742  			// Expected result.
   743  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   744  		},
   745  		{
   746  			// Non-standard case where a transaction record is created during a
   747  			// non-parallel commit. The record already exists because of a
   748  			// failed parallel commit attempt in a prior epoch. The commit will
   749  			// fail because of the pushed timestamp.
   750  			name: "record staging, try commit at higher epoch and pushed timestamp",
   751  			// Replica state.
   752  			existingTxn: stagingRecord,
   753  			// Request state.
   754  			headerTxn: restartedAndPushedHeaderTxn,
   755  			commit:    true,
   756  			// Expected result.
   757  			expError: "TransactionRetryError: retry txn (RETRY_SERIALIZABLE)",
   758  		},
   759  		{
   760  			// The transaction has already been aborted. The client will often
   761  			// send a rollback to resolve any intents and start cleaning up the
   762  			// transaction.
   763  			name: "record aborted, try rollback",
   764  			// Replica state.
   765  			existingTxn: abortedRecord,
   766  			// Request state.
   767  			headerTxn: headerTxn,
   768  			commit:    false,
   769  			// Expected result.
   770  			expTxn: abortedRecord,
   771  		},
   772  		///////////////////////////////////////////////////////////////////////
   773  		//                    INVALID REQUEST ERROR CASES                    //
   774  		///////////////////////////////////////////////////////////////////////
   775  		{
   776  			name: "record pending, try rollback at lower epoch",
   777  			// Replica state.
   778  			existingTxn: func() *roachpb.TransactionRecord {
   779  				record := *pendingRecord
   780  				record.Epoch++
   781  				return &record
   782  			}(),
   783  			// Request state.
   784  			headerTxn: headerTxn,
   785  			commit:    false,
   786  			// Expected result.
   787  			expError: "programming error: epoch regression",
   788  		},
   789  		{
   790  			name: "record pending, try stage at lower epoch",
   791  			// Replica state.
   792  			existingTxn: func() *roachpb.TransactionRecord {
   793  				record := *pendingRecord
   794  				record.Epoch++
   795  				return &record
   796  			}(),
   797  			// Request state.
   798  			headerTxn:      headerTxn,
   799  			commit:         true,
   800  			inFlightWrites: writes,
   801  			// Expected result.
   802  			expError: "programming error: epoch regression",
   803  		},
   804  		{
   805  			name: "record pending, try commit at lower epoch",
   806  			// Replica state.
   807  			existingTxn: func() *roachpb.TransactionRecord {
   808  				record := *pendingRecord
   809  				record.Epoch++
   810  				return &record
   811  			}(),
   812  			// Request state.
   813  			headerTxn: headerTxn,
   814  			commit:    true,
   815  			// Expected result.
   816  			expError: "programming error: epoch regression",
   817  		},
   818  		{
   819  			name: "record committed, try rollback",
   820  			// Replica state.
   821  			existingTxn: committedRecord,
   822  			// Request state.
   823  			headerTxn: headerTxn,
   824  			commit:    false,
   825  			// Expected result.
   826  			expError: "TransactionStatusError: already committed (REASON_TXN_COMMITTED)",
   827  		},
   828  		{
   829  			name: "record committed, try stage",
   830  			// Replica state.
   831  			existingTxn: committedRecord,
   832  			// Request state.
   833  			headerTxn:      headerTxn,
   834  			commit:         true,
   835  			inFlightWrites: writes,
   836  			// Expected result.
   837  			expError: "TransactionStatusError: already committed (REASON_TXN_COMMITTED)",
   838  		},
   839  		{
   840  			name: "record committed, try commit",
   841  			// Replica state.
   842  			existingTxn: committedRecord,
   843  			// Request state.
   844  			headerTxn: headerTxn,
   845  			commit:    true,
   846  			// Expected result.
   847  			expError: "TransactionStatusError: already committed (REASON_TXN_COMMITTED)",
   848  		},
   849  		{
   850  			name: "record aborted, try stage",
   851  			// Replica state.
   852  			existingTxn: abortedRecord,
   853  			// Request state.
   854  			headerTxn:      headerTxn,
   855  			commit:         true,
   856  			inFlightWrites: writes,
   857  			// Expected result.
   858  			expError: "TransactionAbortedError(ABORT_REASON_ABORTED_RECORD_FOUND)",
   859  		},
   860  		{
   861  			name: "record aborted, try commit",
   862  			// Replica state.
   863  			existingTxn: abortedRecord,
   864  			// Request state.
   865  			headerTxn: headerTxn,
   866  			commit:    true,
   867  			// Expected result.
   868  			expError: "TransactionAbortedError(ABORT_REASON_ABORTED_RECORD_FOUND)",
   869  		},
   870  	}
   871  	for _, c := range testCases {
   872  		t.Run(c.name, func(t *testing.T) {
   873  			db := storage.NewDefaultInMem()
   874  			defer db.Close()
   875  			batch := db.NewBatch()
   876  			defer batch.Close()
   877  
   878  			// Write the existing transaction record, if necessary.
   879  			txnKey := keys.TransactionKey(txn.Key, txn.ID)
   880  			if c.existingTxn != nil {
   881  				if err := storage.MVCCPutProto(ctx, batch, nil, txnKey, hlc.Timestamp{}, nil, c.existingTxn); err != nil {
   882  					t.Fatal(err)
   883  				}
   884  			}
   885  
   886  			// Sanity check request args.
   887  			if !c.commit {
   888  				require.Nil(t, c.inFlightWrites)
   889  				require.Nil(t, c.deadline)
   890  			}
   891  
   892  			// Issue an EndTxn request.
   893  			req := roachpb.EndTxnRequest{
   894  				RequestHeader: roachpb.RequestHeader{Key: txn.Key},
   895  				Commit:        c.commit,
   896  
   897  				InFlightWrites: c.inFlightWrites,
   898  				Deadline:       c.deadline,
   899  			}
   900  			if !c.noLockSpans {
   901  				req.LockSpans = intents
   902  			}
   903  			var resp roachpb.EndTxnResponse
   904  			_, err := EndTxn(ctx, batch, CommandArgs{
   905  				EvalCtx: (&MockEvalCtx{
   906  					Desc:      &desc,
   907  					AbortSpan: as,
   908  					CanCreateTxn: func() (bool, hlc.Timestamp, roachpb.TransactionAbortedReason) {
   909  						require.NotNil(t, c.canCreateTxn, "CanCreateTxnRecord unexpectedly called")
   910  						if can, minTS := c.canCreateTxn(); can {
   911  							return true, minTS, 0
   912  						}
   913  						return false, hlc.Timestamp{}, roachpb.ABORT_REASON_ABORTED_RECORD_FOUND
   914  					},
   915  				}).EvalContext(),
   916  				Args: &req,
   917  				Header: roachpb.Header{
   918  					Timestamp: ts,
   919  					Txn:       c.headerTxn,
   920  				},
   921  			}, &resp)
   922  
   923  			if c.expError != "" {
   924  				if !testutils.IsError(err, regexp.QuoteMeta(c.expError)) {
   925  					t.Fatalf("expected error %q; found %v", c.expError, err)
   926  				}
   927  			} else {
   928  				if err != nil {
   929  					t.Fatal(err)
   930  				}
   931  
   932  				// Assert that the txn record is written as expected.
   933  				var resTxnRecord roachpb.TransactionRecord
   934  				if ok, err := storage.MVCCGetProto(
   935  					ctx, batch, txnKey, hlc.Timestamp{}, &resTxnRecord, storage.MVCCGetOptions{},
   936  				); err != nil {
   937  					t.Fatal(err)
   938  				} else if c.expTxn == nil {
   939  					require.False(t, ok, "unexpected transaction record found")
   940  				} else {
   941  					require.True(t, ok, "expected transaction record, one not found")
   942  					require.Equal(t, *c.expTxn, resTxnRecord)
   943  				}
   944  			}
   945  		})
   946  	}
   947  }
   948  
   949  // TestPartialRollbackOnEndTransaction verifies that the intent
   950  // resolution performed synchronously as a side effect of
   951  // EndTransaction request properly takes into account the ignored
   952  // seqnum list.
   953  func TestPartialRollbackOnEndTransaction(t *testing.T) {
   954  	defer leaktest.AfterTest(t)()
   955  
   956  	ctx := context.Background()
   957  	k := roachpb.Key("a")
   958  	ts := hlc.Timestamp{WallTime: 1}
   959  	ts2 := hlc.Timestamp{WallTime: 2}
   960  	txn := roachpb.MakeTransaction("test", k, 0, ts, 0)
   961  	endKey := roachpb.Key("z")
   962  	desc := roachpb.RangeDescriptor{
   963  		RangeID:  99,
   964  		StartKey: roachpb.RKey(k),
   965  		EndKey:   roachpb.RKey(endKey),
   966  	}
   967  	intents := []roachpb.Span{{Key: k}}
   968  
   969  	// We want to inspect the final txn record after EndTxn, to
   970  	// ascertain that it persists the ignore list.
   971  	defer TestingSetTxnAutoGC(false)()
   972  
   973  	testutils.RunTrueAndFalse(t, "withStoredTxnRecord", func(t *testing.T, storeTxnBeforeEndTxn bool) {
   974  		db := storage.NewDefaultInMem()
   975  		defer db.Close()
   976  		batch := db.NewBatch()
   977  		defer batch.Close()
   978  
   979  		var v roachpb.Value
   980  
   981  		// Write a first value at key.
   982  		v.SetString("a")
   983  		txn.Sequence = 1
   984  		if err := storage.MVCCPut(ctx, batch, nil, k, ts, v, &txn); err != nil {
   985  			t.Fatal(err)
   986  		}
   987  		// Write another value.
   988  		v.SetString("b")
   989  		txn.Sequence = 2
   990  		if err := storage.MVCCPut(ctx, batch, nil, k, ts, v, &txn); err != nil {
   991  			t.Fatal(err)
   992  		}
   993  
   994  		// Partially revert the store above.
   995  		txn.IgnoredSeqNums = []enginepb.IgnoredSeqNumRange{{Start: 2, End: 2}}
   996  
   997  		// We test with and without a stored txn record, so as to exercise
   998  		// the two branches of EndTxn() and verify that the ignored seqnum
   999  		// list is properly persisted in the stored transaction record.
  1000  		txnKey := keys.TransactionKey(txn.Key, txn.ID)
  1001  		if storeTxnBeforeEndTxn {
  1002  			txnRec := txn.AsRecord()
  1003  			if err := storage.MVCCPutProto(ctx, batch, nil, txnKey, hlc.Timestamp{}, nil, &txnRec); err != nil {
  1004  				t.Fatal(err)
  1005  			}
  1006  		}
  1007  
  1008  		// Issue the end txn command.
  1009  		req := roachpb.EndTxnRequest{
  1010  			RequestHeader:              roachpb.RequestHeader{Key: txn.Key},
  1011  			Commit:                     true,
  1012  			CanCommitAtHigherTimestamp: true,
  1013  			LockSpans:                  intents,
  1014  		}
  1015  		var resp roachpb.EndTxnResponse
  1016  		if _, err := EndTxn(ctx, batch, CommandArgs{
  1017  			EvalCtx: (&MockEvalCtx{
  1018  				Desc: &desc,
  1019  				CanCreateTxn: func() (bool, hlc.Timestamp, roachpb.TransactionAbortedReason) {
  1020  					return true, ts, 0
  1021  				},
  1022  			}).EvalContext(),
  1023  			Args: &req,
  1024  			Header: roachpb.Header{
  1025  				Timestamp: ts,
  1026  				Txn:       &txn,
  1027  			},
  1028  		}, &resp); err != nil {
  1029  			t.Fatal(err)
  1030  		}
  1031  
  1032  		// The second write has been rolled back; verify that the remaining
  1033  		// value is from the first write.
  1034  		res, i, err := storage.MVCCGet(ctx, batch, k, ts2, storage.MVCCGetOptions{})
  1035  		if err != nil {
  1036  			t.Fatal(err)
  1037  		}
  1038  		if i != nil {
  1039  			t.Errorf("found intent, expected none: %+v", i)
  1040  		}
  1041  		if res == nil {
  1042  			t.Errorf("no value found, expected one")
  1043  		} else {
  1044  			s, err := res.GetBytes()
  1045  			if err != nil {
  1046  				t.Fatal(err)
  1047  			}
  1048  			require.Equal(t, "a", string(s))
  1049  		}
  1050  
  1051  		// Also verify that the txn record contains the ignore list.
  1052  		var txnRec roachpb.TransactionRecord
  1053  		hasRec, err := storage.MVCCGetProto(ctx, batch, txnKey, hlc.Timestamp{}, &txnRec, storage.MVCCGetOptions{})
  1054  		if err != nil {
  1055  			t.Fatal(err)
  1056  		}
  1057  		if !hasRec {
  1058  			t.Error("expected txn record remaining after test, found none")
  1059  		} else {
  1060  			require.Equal(t, txn.IgnoredSeqNums, txnRec.IgnoredSeqNums)
  1061  		}
  1062  	})
  1063  }