github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvclient/kvcoord/txn_interceptor_committer_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 kvcoord
    12  
    13  import (
    14  	"context"
    15  	"testing"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    18  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    19  	"github.com/cockroachdb/cockroach/pkg/testutils"
    20  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    21  	"github.com/cockroachdb/cockroach/pkg/util/stop"
    22  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  func makeMockTxnCommitter() (txnCommitter, *mockLockedSender) {
    27  	mockSender := &mockLockedSender{}
    28  	return txnCommitter{
    29  		st:      cluster.MakeTestingClusterSettings(),
    30  		stopper: stop.NewStopper(),
    31  		wrapped: mockSender,
    32  		mu:      new(syncutil.Mutex),
    33  	}, mockSender
    34  }
    35  
    36  // TestTxnCommitterElideEndTxn tests that EndTxn requests for read-only
    37  // transactions are removed from their batches because they are not necessary.
    38  // The test verifies the case where the EndTxn request is part of a batch with
    39  // other requests and the case where it is alone in its batch.
    40  func TestTxnCommitterElideEndTxn(t *testing.T) {
    41  	defer leaktest.AfterTest(t)()
    42  	ctx := context.Background()
    43  	tc, mockSender := makeMockTxnCommitter()
    44  	defer tc.stopper.Stop(ctx)
    45  
    46  	txn := makeTxnProto()
    47  	keyA := roachpb.Key("a")
    48  
    49  	// Test with both commits and rollbacks.
    50  	testutils.RunTrueAndFalse(t, "commit", func(t *testing.T, commit bool) {
    51  		expStatus := roachpb.COMMITTED
    52  		if !commit {
    53  			expStatus = roachpb.ABORTED
    54  		}
    55  
    56  		// Test the case where the EndTxn request is part of a larger batch of
    57  		// requests.
    58  		var ba roachpb.BatchRequest
    59  		ba.Header = roachpb.Header{Txn: &txn}
    60  		ba.Add(&roachpb.GetRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}})
    61  		ba.Add(&roachpb.PutRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}})
    62  		ba.Add(&roachpb.EndTxnRequest{Commit: commit, LockSpans: nil})
    63  
    64  		mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
    65  			require.Len(t, ba.Requests, 2)
    66  			require.IsType(t, &roachpb.GetRequest{}, ba.Requests[0].GetInner())
    67  			require.IsType(t, &roachpb.PutRequest{}, ba.Requests[1].GetInner())
    68  
    69  			br := ba.CreateReply()
    70  			br.Txn = ba.Txn
    71  			// The Sender did not receive an EndTxn request, so it keeps the Txn
    72  			// status as PENDING.
    73  			br.Txn.Status = roachpb.PENDING
    74  			return br, nil
    75  		})
    76  
    77  		br, pErr := tc.SendLocked(ctx, ba)
    78  		require.Nil(t, pErr)
    79  		require.NotNil(t, br)
    80  		require.NotNil(t, br.Txn)
    81  		require.Equal(t, expStatus, br.Txn.Status)
    82  
    83  		// Test the case where the EndTxn request is alone.
    84  		ba.Requests = nil
    85  		ba.Add(&roachpb.EndTxnRequest{Commit: commit, LockSpans: nil})
    86  
    87  		mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
    88  			require.Fail(t, "should not have issued batch request", ba)
    89  			return nil, nil
    90  		})
    91  
    92  		br, pErr = tc.SendLocked(ctx, ba)
    93  		require.Nil(t, pErr)
    94  		require.NotNil(t, br)
    95  		require.NotNil(t, br.Txn)
    96  		require.Equal(t, expStatus, br.Txn.Status)
    97  	})
    98  }
    99  
   100  // TestTxnCommitterAttachesTxnKey tests that the txnCommitter attaches the
   101  // transaction key to committing and aborting EndTxn requests.
   102  func TestTxnCommitterAttachesTxnKey(t *testing.T) {
   103  	defer leaktest.AfterTest(t)()
   104  	ctx := context.Background()
   105  	tc, mockSender := makeMockTxnCommitter()
   106  	defer tc.stopper.Stop(ctx)
   107  
   108  	txn := makeTxnProto()
   109  	keyA := roachpb.Key("a")
   110  
   111  	// Attach LockSpans to each EndTxn request to avoid the elided EndTxn
   112  	// optimization.
   113  	intents := []roachpb.Span{{Key: keyA}}
   114  
   115  	// Verify that the txn key is attached to committing EndTxn requests.
   116  	var ba roachpb.BatchRequest
   117  	ba.Header = roachpb.Header{Txn: &txn}
   118  	ba.Add(&roachpb.PutRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}})
   119  	ba.Add(&roachpb.EndTxnRequest{Commit: true, LockSpans: intents})
   120  
   121  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   122  		require.Len(t, ba.Requests, 2)
   123  		require.Equal(t, keyA, ba.Requests[0].GetInner().Header().Key)
   124  		require.Equal(t, roachpb.Key(txn.Key), ba.Requests[1].GetInner().Header().Key)
   125  
   126  		br := ba.CreateReply()
   127  		br.Txn = ba.Txn
   128  		br.Txn.Status = roachpb.COMMITTED
   129  		return br, nil
   130  	})
   131  
   132  	br, pErr := tc.SendLocked(ctx, ba)
   133  	require.Nil(t, pErr)
   134  	require.NotNil(t, br)
   135  
   136  	// Verify that the txn key is attached to aborting EndTxn requests.
   137  	ba.Requests = nil
   138  	ba.Add(&roachpb.EndTxnRequest{Commit: false, LockSpans: intents})
   139  
   140  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   141  		require.Len(t, ba.Requests, 1)
   142  		require.Equal(t, roachpb.Key(txn.Key), ba.Requests[0].GetInner().Header().Key)
   143  
   144  		br = ba.CreateReply()
   145  		br.Txn = ba.Txn
   146  		br.Txn.Status = roachpb.ABORTED
   147  		return br, nil
   148  	})
   149  
   150  	br, pErr = tc.SendLocked(ctx, ba)
   151  	require.Nil(t, pErr)
   152  	require.NotNil(t, br)
   153  }
   154  
   155  // TestTxnCommitterStripsInFlightWrites tests that the txnCommitter strips the
   156  // pipelined writes that have yet to be proven and the new writes that are part
   157  // of the same batch as an EndTxn request from the in-flight write set when a
   158  // parallel commit is not desired. It also tests that it keeps the in-flight
   159  // writes attached to the EndTxn request when a parallel commit is desired.
   160  func TestTxnCommitterStripsInFlightWrites(t *testing.T) {
   161  	defer leaktest.AfterTest(t)()
   162  
   163  	ctx := context.Background()
   164  	tc, mockSender := makeMockTxnCommitter()
   165  	defer tc.stopper.Stop(ctx)
   166  
   167  	// Start with parallel commits disabled. Should NOT attach in-flight writes.
   168  	parallelCommitsEnabled.Override(&tc.st.SV, false)
   169  
   170  	txn := makeTxnProto()
   171  	keyA, keyB := roachpb.Key("a"), roachpb.Key("b")
   172  
   173  	// Verify that the QueryIntent and the Put are both attached as lock spans
   174  	// to the committing EndTxn request when expected.
   175  	var ba roachpb.BatchRequest
   176  	ba.Header = roachpb.Header{Txn: &txn}
   177  	qiArgs := roachpb.QueryIntentRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}}
   178  	putArgs := roachpb.PutRequest{RequestHeader: roachpb.RequestHeader{Key: keyB}}
   179  	etArgs := roachpb.EndTxnRequest{Commit: true}
   180  	qiArgs.Txn.Sequence = 1
   181  	putArgs.Sequence = 2
   182  	etArgs.Sequence = 3
   183  	etArgs.InFlightWrites = []roachpb.SequencedWrite{
   184  		{Key: keyA, Sequence: 1}, {Key: keyB, Sequence: 2},
   185  	}
   186  	etArgsCopy := etArgs
   187  	ba.Add(&putArgs, &qiArgs, &etArgsCopy)
   188  
   189  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   190  		require.Len(t, ba.Requests, 3)
   191  		require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[2].GetInner())
   192  
   193  		et := ba.Requests[2].GetInner().(*roachpb.EndTxnRequest)
   194  		require.True(t, et.Commit)
   195  		require.Len(t, et.LockSpans, 2)
   196  		require.Equal(t, []roachpb.Span{{Key: keyA}, {Key: keyB}}, et.LockSpans)
   197  		require.Len(t, et.InFlightWrites, 0)
   198  
   199  		br := ba.CreateReply()
   200  		br.Txn = ba.Txn
   201  		br.Txn.Status = roachpb.COMMITTED
   202  		return br, nil
   203  	})
   204  
   205  	br, pErr := tc.SendLocked(ctx, ba)
   206  	require.Nil(t, pErr)
   207  	require.NotNil(t, br)
   208  
   209  	// Enable parallel commits and send the same batch. Should attach in-flight writes.
   210  	parallelCommitsEnabled.Override(&tc.st.SV, true)
   211  
   212  	ba.Requests = nil
   213  	etArgsCopy = etArgs
   214  	ba.Add(&putArgs, &qiArgs, &etArgsCopy)
   215  
   216  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   217  		require.Len(t, ba.Requests, 3)
   218  		require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[2].GetInner())
   219  
   220  		et := ba.Requests[2].GetInner().(*roachpb.EndTxnRequest)
   221  		require.True(t, et.Commit)
   222  		require.Len(t, et.InFlightWrites, 2)
   223  		require.Equal(t, roachpb.SequencedWrite{Key: keyA, Sequence: 1}, et.InFlightWrites[0])
   224  		require.Equal(t, roachpb.SequencedWrite{Key: keyB, Sequence: 2}, et.InFlightWrites[1])
   225  
   226  		br = ba.CreateReply()
   227  		br.Txn = ba.Txn
   228  		br.Txn.Status = roachpb.COMMITTED
   229  		return br, nil
   230  	})
   231  
   232  	br, pErr = tc.SendLocked(ctx, ba)
   233  	require.Nil(t, pErr)
   234  	require.NotNil(t, br)
   235  
   236  	// Send the same batch but with an EndTxn containing a commit trigger.
   237  	// In-flight writes should not be attached because commit triggers disable
   238  	// parallel commits.
   239  	ba.Requests = nil
   240  	etArgsWithTrigger := etArgs
   241  	etArgsWithTrigger.InternalCommitTrigger = &roachpb.InternalCommitTrigger{
   242  		ModifiedSpanTrigger: &roachpb.ModifiedSpanTrigger{
   243  			SystemConfigSpan: true,
   244  		},
   245  	}
   246  	ba.Add(&putArgs, &qiArgs, &etArgsWithTrigger)
   247  
   248  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   249  		require.Len(t, ba.Requests, 3)
   250  		require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[2].GetInner())
   251  
   252  		et := ba.Requests[2].GetInner().(*roachpb.EndTxnRequest)
   253  		require.True(t, et.Commit)
   254  		require.Len(t, et.LockSpans, 2)
   255  		require.Equal(t, []roachpb.Span{{Key: keyA}, {Key: keyB}}, et.LockSpans)
   256  		require.Len(t, et.InFlightWrites, 0)
   257  
   258  		br = ba.CreateReply()
   259  		br.Txn = ba.Txn
   260  		br.Txn.Status = roachpb.COMMITTED
   261  		return br, nil
   262  	})
   263  
   264  	br, pErr = tc.SendLocked(ctx, ba)
   265  	require.Nil(t, pErr)
   266  	require.NotNil(t, br)
   267  
   268  	// Send the same batch but with a ranged write instead of a point write.
   269  	// In-flight writes should not be attached because ranged writes cannot
   270  	// be parallelized with a commit.
   271  	ba.Requests = nil
   272  	delRngArgs := roachpb.DeleteRangeRequest{RequestHeader: roachpb.RequestHeader{Key: keyA, EndKey: keyB}}
   273  	delRngArgs.Sequence = 2
   274  	etArgsWithRangedIntentSpan := etArgs
   275  	etArgsWithRangedIntentSpan.LockSpans = []roachpb.Span{{Key: keyA, EndKey: keyB}}
   276  	etArgsWithRangedIntentSpan.InFlightWrites = []roachpb.SequencedWrite{{Key: keyA, Sequence: 1}}
   277  	ba.Add(&delRngArgs, &qiArgs, &etArgsWithRangedIntentSpan)
   278  
   279  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   280  		require.Len(t, ba.Requests, 3)
   281  		require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[2].GetInner())
   282  
   283  		et := ba.Requests[2].GetInner().(*roachpb.EndTxnRequest)
   284  		require.True(t, et.Commit)
   285  		require.Len(t, et.LockSpans, 1)
   286  		require.Equal(t, []roachpb.Span{{Key: keyA, EndKey: keyB}}, et.LockSpans)
   287  		require.Len(t, et.InFlightWrites, 0)
   288  
   289  		br = ba.CreateReply()
   290  		br.Txn = ba.Txn
   291  		br.Txn.Status = roachpb.COMMITTED
   292  		return br, nil
   293  	})
   294  
   295  	br, pErr = tc.SendLocked(ctx, ba)
   296  	require.Nil(t, pErr)
   297  	require.NotNil(t, br)
   298  
   299  	// Send the same batch but with a point read instead of a point write.
   300  	// In-flight writes should not be attached because read-only requests
   301  	// cannot be parallelized with a commit.
   302  	ba.Requests = nil
   303  	getArgs := roachpb.GetRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}}
   304  	getArgs.Sequence = 2
   305  	etArgsCopy = etArgs
   306  	ba.Add(&getArgs, &qiArgs, &etArgsCopy)
   307  
   308  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   309  		require.Len(t, ba.Requests, 3)
   310  		require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[2].GetInner())
   311  
   312  		et := ba.Requests[2].GetInner().(*roachpb.EndTxnRequest)
   313  		require.True(t, et.Commit)
   314  		require.Len(t, et.LockSpans, 2)
   315  		require.Equal(t, []roachpb.Span{{Key: keyA}, {Key: keyB}}, et.LockSpans)
   316  		require.Len(t, et.InFlightWrites, 0)
   317  
   318  		br = ba.CreateReply()
   319  		br.Txn = ba.Txn
   320  		br.Txn.Status = roachpb.COMMITTED
   321  		return br, nil
   322  	})
   323  
   324  	br, pErr = tc.SendLocked(ctx, ba)
   325  	require.Nil(t, pErr)
   326  	require.NotNil(t, br)
   327  }
   328  
   329  // TestTxnCommitterAsyncExplicitCommitTask verifies that when txnCommitter
   330  // performs a parallel commit and receives a STAGING transaction status,
   331  // it launches an async task to make the transaction commit explicit.
   332  func TestTxnCommitterAsyncExplicitCommitTask(t *testing.T) {
   333  	defer leaktest.AfterTest(t)()
   334  	ctx := context.Background()
   335  	tc, mockSender := makeMockTxnCommitter()
   336  	defer tc.stopper.Stop(ctx)
   337  
   338  	txn := makeTxnProto()
   339  	keyA := roachpb.Key("a")
   340  
   341  	// Verify that the Put is attached as in-flight write to the committing
   342  	// EndTxn request.
   343  	var ba roachpb.BatchRequest
   344  	ba.Header = roachpb.Header{Txn: &txn}
   345  	putArgs := roachpb.PutRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}}
   346  	etArgs := roachpb.EndTxnRequest{Commit: true}
   347  	putArgs.Sequence = 1
   348  	etArgs.Sequence = 2
   349  	etArgs.InFlightWrites = []roachpb.SequencedWrite{{Key: keyA, Sequence: 1}}
   350  	ba.Add(&putArgs, &etArgs)
   351  
   352  	// Set the CanForwardReadTimestamp and CanCommitAtHigherTimestamp flags so
   353  	// we can make sure that these are propagated to the async explicit commit
   354  	// task.
   355  	ba.Header.CanForwardReadTimestamp = true
   356  	etArgs.CanCommitAtHigherTimestamp = true
   357  
   358  	explicitCommitCh := make(chan struct{})
   359  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   360  		require.Len(t, ba.Requests, 2)
   361  		require.True(t, ba.CanForwardReadTimestamp)
   362  		require.IsType(t, &roachpb.PutRequest{}, ba.Requests[0].GetInner())
   363  		require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[1].GetInner())
   364  
   365  		et := ba.Requests[1].GetInner().(*roachpb.EndTxnRequest)
   366  		require.True(t, et.Commit)
   367  		require.True(t, et.CanCommitAtHigherTimestamp)
   368  		require.Len(t, et.InFlightWrites, 1)
   369  		require.Equal(t, roachpb.SequencedWrite{Key: keyA, Sequence: 1}, et.InFlightWrites[0])
   370  
   371  		br := ba.CreateReply()
   372  		br.Txn = ba.Txn
   373  		br.Txn.Status = roachpb.STAGING
   374  		br.Responses[1].GetInner().(*roachpb.EndTxnResponse).StagingTimestamp = br.Txn.WriteTimestamp
   375  
   376  		// Before returning, mock out the sender again to test against the async
   377  		// task that should be sent to make the implicit txn commit explicit.
   378  		mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   379  			defer close(explicitCommitCh)
   380  			require.Len(t, ba.Requests, 1)
   381  			require.True(t, ba.CanForwardReadTimestamp)
   382  			require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[0].GetInner())
   383  
   384  			et := ba.Requests[0].GetInner().(*roachpb.EndTxnRequest)
   385  			require.True(t, et.Commit)
   386  			require.True(t, et.CanCommitAtHigherTimestamp)
   387  			require.Len(t, et.InFlightWrites, 0)
   388  
   389  			br = ba.CreateReply()
   390  			br.Txn = ba.Txn
   391  			br.Txn.Status = roachpb.COMMITTED
   392  			return br, nil
   393  		})
   394  		return br, nil
   395  	})
   396  
   397  	br, pErr := tc.SendLocked(ctx, ba)
   398  	require.Nil(t, pErr)
   399  	require.NotNil(t, br)
   400  
   401  	// Wait until the explicit commit succeeds.
   402  	<-explicitCommitCh
   403  }
   404  
   405  // TestTxnCommitterRetryAfterStaging verifies that txnCommitter returns a retry
   406  // error when a write performed in parallel with staging a transaction is pushed
   407  // to a timestamp above the staging timestamp.
   408  func TestTxnCommitterRetryAfterStaging(t *testing.T) {
   409  	defer leaktest.AfterTest(t)()
   410  	ctx := context.Background()
   411  	testutils.RunTrueAndFalse(t, "WriteTooOld", func(t *testing.T, writeTooOld bool) {
   412  		tc, mockSender := makeMockTxnCommitter()
   413  		defer tc.stopper.Stop(ctx)
   414  
   415  		txn := makeTxnProto()
   416  		keyA := roachpb.Key("a")
   417  
   418  		var ba roachpb.BatchRequest
   419  		ba.Header = roachpb.Header{Txn: &txn}
   420  		putArgs := roachpb.PutRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}}
   421  		etArgs := roachpb.EndTxnRequest{Commit: true}
   422  		putArgs.Sequence = 1
   423  		etArgs.Sequence = 2
   424  		etArgs.InFlightWrites = []roachpb.SequencedWrite{{Key: keyA, Sequence: 1}}
   425  		ba.Add(&putArgs, &etArgs)
   426  
   427  		mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   428  			require.Len(t, ba.Requests, 2)
   429  			require.IsType(t, &roachpb.PutRequest{}, ba.Requests[0].GetInner())
   430  			require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[1].GetInner())
   431  
   432  			et := ba.Requests[1].GetInner().(*roachpb.EndTxnRequest)
   433  			require.True(t, et.Commit)
   434  			require.Len(t, et.InFlightWrites, 1)
   435  			require.Equal(t, roachpb.SequencedWrite{Key: keyA, Sequence: 1}, et.InFlightWrites[0])
   436  
   437  			br := ba.CreateReply()
   438  			br.Txn = ba.Txn
   439  			br.Txn.Status = roachpb.STAGING
   440  			br.Responses[1].GetInner().(*roachpb.EndTxnResponse).StagingTimestamp = br.Txn.WriteTimestamp
   441  
   442  			// Pretend the PutRequest was split and sent to a different Range. It
   443  			// could hit the timestamp cache, or a WriteTooOld error (which sets the
   444  			// WriteTooOld flag). The intent will be written but the response
   445  			// transaction's timestamp will be larger than the staging timestamp.
   446  			br.Txn.WriteTooOld = writeTooOld
   447  			br.Txn.WriteTimestamp = br.Txn.WriteTimestamp.Add(1, 0)
   448  			return br, nil
   449  		})
   450  
   451  		br, pErr := tc.SendLocked(ctx, ba)
   452  		require.Nil(t, br)
   453  		require.NotNil(t, pErr)
   454  		require.IsType(t, &roachpb.TransactionRetryError{}, pErr.GetDetail())
   455  		expReason := roachpb.RETRY_SERIALIZABLE
   456  		if writeTooOld {
   457  			expReason = roachpb.RETRY_WRITE_TOO_OLD
   458  		}
   459  		require.Equal(t, expReason, pErr.GetDetail().(*roachpb.TransactionRetryError).Reason)
   460  	})
   461  }
   462  
   463  // Test that parallel commits are inhibited on retries (i.e. after a successful
   464  // refresh caused by a parallel-commit batch). See comments in the interceptor
   465  // about why this is necessary.
   466  func TestTxnCommitterNoParallelCommitsOnRetry(t *testing.T) {
   467  	defer leaktest.AfterTest(t)()
   468  	ctx := context.Background()
   469  	tc, mockSender := makeMockTxnCommitter()
   470  	defer tc.stopper.Stop(ctx)
   471  
   472  	txn := makeTxnProto()
   473  	keyA := roachpb.Key("a")
   474  
   475  	var ba roachpb.BatchRequest
   476  	ba.Header = roachpb.Header{Txn: &txn}
   477  	putArgs := roachpb.PutRequest{RequestHeader: roachpb.RequestHeader{Key: keyA}}
   478  	etArgs := roachpb.EndTxnRequest{Commit: true}
   479  	putArgs.Sequence = 1
   480  	etArgs.Sequence = 2
   481  	etArgs.InFlightWrites = []roachpb.SequencedWrite{{Key: keyA, Sequence: 1}}
   482  
   483  	// Pretend that this is a retry of the request (after a successful refresh). Having the key
   484  	// assigned is how the interceptor distinguishes retries.
   485  	etArgs.Key = keyA
   486  
   487  	ba.Add(&putArgs, &etArgs)
   488  
   489  	mockSender.MockSend(func(ba roachpb.BatchRequest) (*roachpb.BatchResponse, *roachpb.Error) {
   490  		require.Len(t, ba.Requests, 2)
   491  		require.IsType(t, &roachpb.PutRequest{}, ba.Requests[0].GetInner())
   492  		require.IsType(t, &roachpb.EndTxnRequest{}, ba.Requests[1].GetInner())
   493  
   494  		et := ba.Requests[1].GetInner().(*roachpb.EndTxnRequest)
   495  		require.True(t, et.Commit)
   496  		require.Len(t, et.InFlightWrites, 0, "expected parallel commit to be inhibited")
   497  
   498  		br := ba.CreateReply()
   499  		br.Txn = ba.Txn
   500  		br.Txn.Status = roachpb.COMMITTED
   501  		return br, nil
   502  	})
   503  
   504  	_, pErr := tc.SendLocked(ctx, ba)
   505  	require.Nil(t, pErr)
   506  }