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 }