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 }