github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/kv/kvserver/batcheval/cmd_recover_txn_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  	"testing"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/keys"
    18  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    19  	"github.com/cockroachdb/cockroach/pkg/storage"
    20  	"github.com/cockroachdb/cockroach/pkg/testutils"
    21  	"github.com/cockroachdb/cockroach/pkg/util/hlc"
    22  	"github.com/cockroachdb/cockroach/pkg/util/leaktest"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  // TestRecoverTxn tests RecoverTxn request in its base case where no concurrent
    27  // actors have modified the transaction record that it is attempting to recover.
    28  // It tests the case where all of the txn's in-flight writes were successful and
    29  // the case where one of the txn's in-flight writes was found missing and
    30  // prevented.
    31  func TestRecoverTxn(t *testing.T) {
    32  	defer leaktest.AfterTest(t)()
    33  
    34  	ctx := context.Background()
    35  	k, k2 := roachpb.Key("a"), roachpb.Key("b")
    36  	ts := hlc.Timestamp{WallTime: 1}
    37  	txn := roachpb.MakeTransaction("test", k, 0, ts, 0)
    38  	txn.Status = roachpb.STAGING
    39  	txn.LockSpans = []roachpb.Span{{Key: k}}
    40  	txn.InFlightWrites = []roachpb.SequencedWrite{{Key: k2, Sequence: 0}}
    41  
    42  	testutils.RunTrueAndFalse(t, "missing write", func(t *testing.T, missingWrite bool) {
    43  		db := storage.NewDefaultInMem()
    44  		defer db.Close()
    45  
    46  		// Write the transaction record.
    47  		txnKey := keys.TransactionKey(txn.Key, txn.ID)
    48  		txnRecord := txn.AsRecord()
    49  		if err := storage.MVCCPutProto(ctx, db, nil, txnKey, hlc.Timestamp{}, nil, &txnRecord); err != nil {
    50  			t.Fatal(err)
    51  		}
    52  
    53  		// Issue a RecoverTxn request.
    54  		var resp roachpb.RecoverTxnResponse
    55  		if _, err := RecoverTxn(ctx, db, CommandArgs{
    56  			Args: &roachpb.RecoverTxnRequest{
    57  				RequestHeader:       roachpb.RequestHeader{Key: txn.Key},
    58  				Txn:                 txn.TxnMeta,
    59  				ImplicitlyCommitted: !missingWrite,
    60  			},
    61  			Header: roachpb.Header{
    62  				Timestamp: ts,
    63  			},
    64  		}, &resp); err != nil {
    65  			t.Fatal(err)
    66  		}
    67  
    68  		// Assert that the response is correct.
    69  		expTxnRecord := txn.AsRecord()
    70  		expTxn := expTxnRecord.AsTransaction()
    71  		// Merge the in-flight writes into the lock spans.
    72  		expTxn.LockSpans = []roachpb.Span{{Key: k}, {Key: k2}}
    73  		expTxn.InFlightWrites = nil
    74  		// Set the correct status.
    75  		if !missingWrite {
    76  			expTxn.Status = roachpb.COMMITTED
    77  		} else {
    78  			expTxn.Status = roachpb.ABORTED
    79  		}
    80  		require.Equal(t, expTxn, resp.RecoveredTxn)
    81  
    82  		// Assert that the updated txn record was persisted correctly.
    83  		var resTxnRecord roachpb.Transaction
    84  		if _, err := storage.MVCCGetProto(
    85  			ctx, db, txnKey, hlc.Timestamp{}, &resTxnRecord, storage.MVCCGetOptions{},
    86  		); err != nil {
    87  			t.Fatal(err)
    88  		}
    89  		require.Equal(t, expTxn, resTxnRecord)
    90  	})
    91  }
    92  
    93  // TestRecoverTxnRecordChanged tests that RecoverTxn requests are no-ops when
    94  // they find that the transaction record that they are attempting to recover is
    95  // different than what they expected it to be, which would be either due to an
    96  // active transaction coordinator or due to a concurrent recovery.
    97  func TestRecoverTxnRecordChanged(t *testing.T) {
    98  	defer leaktest.AfterTest(t)()
    99  
   100  	ctx := context.Background()
   101  	k := roachpb.Key("a")
   102  	ts := hlc.Timestamp{WallTime: 1}
   103  	txn := roachpb.MakeTransaction("test", k, 0, ts, 0)
   104  	txn.Status = roachpb.STAGING
   105  
   106  	testCases := []struct {
   107  		name                string
   108  		implicitlyCommitted bool
   109  		expError            string
   110  		changedTxn          roachpb.Transaction
   111  	}{
   112  		{
   113  			name:                "transaction commit after all writes found",
   114  			implicitlyCommitted: true,
   115  			changedTxn: func() roachpb.Transaction {
   116  				txnCopy := txn
   117  				txnCopy.Status = roachpb.COMMITTED
   118  				txnCopy.InFlightWrites = nil
   119  				return txnCopy
   120  			}(),
   121  		},
   122  		{
   123  			name:                "transaction abort after all writes found",
   124  			implicitlyCommitted: true,
   125  			expError:            "found ABORTED record for implicitly committed transaction",
   126  			changedTxn: func() roachpb.Transaction {
   127  				txnCopy := txn
   128  				txnCopy.Status = roachpb.ABORTED
   129  				txnCopy.InFlightWrites = nil
   130  				return txnCopy
   131  			}(),
   132  		},
   133  		{
   134  			name:                "transaction restart after all writes found",
   135  			implicitlyCommitted: true,
   136  			expError:            "epoch change by implicitly committed transaction: 0->1",
   137  			changedTxn: func() roachpb.Transaction {
   138  				txnCopy := txn
   139  				txnCopy.BumpEpoch()
   140  				return txnCopy
   141  			}(),
   142  		},
   143  		{
   144  			name:                "transaction timestamp increase after all writes found",
   145  			implicitlyCommitted: true,
   146  			expError:            "timestamp change by implicitly committed transaction: 0.000000001,0->0.000000002,0",
   147  			changedTxn: func() roachpb.Transaction {
   148  				txnCopy := txn
   149  				txnCopy.WriteTimestamp = txnCopy.WriteTimestamp.Add(1, 0)
   150  				return txnCopy
   151  			}(),
   152  		},
   153  		{
   154  			name:                "transaction commit after write prevented",
   155  			implicitlyCommitted: false,
   156  			changedTxn: func() roachpb.Transaction {
   157  				txnCopy := txn
   158  				txnCopy.Status = roachpb.COMMITTED
   159  				txnCopy.InFlightWrites = nil
   160  				return txnCopy
   161  			}(),
   162  		},
   163  		{
   164  			name:                "transaction abort after write prevented",
   165  			implicitlyCommitted: false,
   166  			changedTxn: func() roachpb.Transaction {
   167  				txnCopy := txn
   168  				txnCopy.Status = roachpb.ABORTED
   169  				txnCopy.InFlightWrites = nil
   170  				return txnCopy
   171  			}(),
   172  		},
   173  		{
   174  			name:                "transaction restart after write prevented",
   175  			implicitlyCommitted: false,
   176  			changedTxn: func() roachpb.Transaction {
   177  				txnCopy := txn
   178  				txnCopy.BumpEpoch()
   179  				return txnCopy
   180  			}(),
   181  		},
   182  		{
   183  			name:                "transaction timestamp increase after write prevented",
   184  			implicitlyCommitted: false,
   185  			changedTxn: func() roachpb.Transaction {
   186  				txnCopy := txn
   187  				txnCopy.WriteTimestamp = txnCopy.WriteTimestamp.Add(1, 0)
   188  				return txnCopy
   189  			}(),
   190  		},
   191  	}
   192  	for _, c := range testCases {
   193  		t.Run(c.name, func(t *testing.T) {
   194  			db := storage.NewDefaultInMem()
   195  			defer db.Close()
   196  
   197  			// Write the modified transaction record, simulating a concurrent
   198  			// actor changing the transaction record before the RecoverTxn
   199  			// request is evaluated.
   200  			txnKey := keys.TransactionKey(txn.Key, txn.ID)
   201  			txnRecord := c.changedTxn.AsRecord()
   202  			if err := storage.MVCCPutProto(ctx, db, nil, txnKey, hlc.Timestamp{}, nil, &txnRecord); err != nil {
   203  				t.Fatal(err)
   204  			}
   205  
   206  			// Issue a RecoverTxn request.
   207  			var resp roachpb.RecoverTxnResponse
   208  			_, err := RecoverTxn(ctx, db, CommandArgs{
   209  				Args: &roachpb.RecoverTxnRequest{
   210  					RequestHeader:       roachpb.RequestHeader{Key: txn.Key},
   211  					Txn:                 txn.TxnMeta,
   212  					ImplicitlyCommitted: c.implicitlyCommitted,
   213  				},
   214  				Header: roachpb.Header{
   215  					Timestamp: ts,
   216  				},
   217  			}, &resp)
   218  
   219  			if c.expError != "" {
   220  				if !testutils.IsError(err, c.expError) {
   221  					t.Fatalf("expected error %q; found %v", c.expError, err)
   222  				}
   223  			} else {
   224  				if err != nil {
   225  					t.Fatal(err)
   226  				}
   227  
   228  				// Assert that the response is correct.
   229  				expTxnRecord := c.changedTxn.AsRecord()
   230  				expTxn := expTxnRecord.AsTransaction()
   231  				require.Equal(t, expTxn, resp.RecoveredTxn)
   232  
   233  				// Assert that the txn record was not modified.
   234  				var resTxnRecord roachpb.Transaction
   235  				if _, err := storage.MVCCGetProto(
   236  					ctx, db, txnKey, hlc.Timestamp{}, &resTxnRecord, storage.MVCCGetOptions{},
   237  				); err != nil {
   238  					t.Fatal(err)
   239  				}
   240  				require.Equal(t, expTxn, resTxnRecord)
   241  			}
   242  		})
   243  	}
   244  }