@@ -417,6 +417,203 @@ func TestTxnHeartbeaterAsyncAbort(t *testing.T) {
417
417
})
418
418
}
419
419
420
+ // TestTxnHeartbeaterAsyncAbortWaitsForInFlight tests that the txnHeartbeater
421
+ // will wait for an in-flight request to complete before sending the
422
+ // EndTxn rollback request.
423
+ func TestTxnHeartbeaterAsyncAbortWaitsForInFlight (t * testing.T ) {
424
+ defer leaktest .AfterTest (t )()
425
+ defer log .Scope (t ).Close (t )
426
+ ctx := context .Background ()
427
+ txn := makeTxnProto ()
428
+ th , mockSender , mockGatekeeper := makeMockTxnHeartbeater (& txn )
429
+ defer th .stopper .Stop (ctx )
430
+
431
+ // Mock the heartbeat request, which should wait for an in-flight put via
432
+ // putReady then return an aborted txn and signal hbAborted.
433
+ putReady := make (chan struct {})
434
+ hbAborted := make (chan struct {})
435
+ mockGatekeeper .MockSend (func (ba roachpb.BatchRequest ) (* roachpb.BatchResponse , * roachpb.Error ) {
436
+ <- putReady
437
+ defer close (hbAborted )
438
+
439
+ require .Len (t , ba .Requests , 1 )
440
+ require .IsType (t , & roachpb.HeartbeatTxnRequest {}, ba .Requests [0 ].GetInner ())
441
+
442
+ br := ba .CreateReply ()
443
+ br .Txn = ba .Txn
444
+ br .Txn .Status = roachpb .ABORTED
445
+ return br , nil
446
+ })
447
+
448
+ putResume := make (chan struct {})
449
+ rollbackSent := make (chan struct {})
450
+ mockSender .ChainMockSend (
451
+ // Mock a Put, which signals putReady and then waits for putResume
452
+ // before returning a response.
453
+ func (ba roachpb.BatchRequest ) (* roachpb.BatchResponse , * roachpb.Error ) {
454
+ th .mu .Unlock () // without txnLockGatekeeper, we must unlock manually
455
+ defer th .mu .Lock ()
456
+ close (putReady )
457
+ require .Len (t , ba .Requests , 1 )
458
+ require .IsType (t , & roachpb.PutRequest {}, ba .Requests [0 ].GetInner ())
459
+
460
+ <- putResume
461
+
462
+ br := ba .CreateReply ()
463
+ br .Txn = ba .Txn
464
+ return br , nil
465
+ },
466
+ // Mock an EndTxn, which signals rollbackSent.
467
+ func (ba roachpb.BatchRequest ) (* roachpb.BatchResponse , * roachpb.Error ) {
468
+ defer close (rollbackSent )
469
+ require .Len (t , ba .Requests , 1 )
470
+ require .IsType (t , & roachpb.EndTxnRequest {}, ba .Requests [0 ].GetInner ())
471
+
472
+ etReq := ba .Requests [0 ].GetInner ().(* roachpb.EndTxnRequest )
473
+ require .Equal (t , & txn , ba .Txn )
474
+ require .False (t , etReq .Commit )
475
+ require .True (t , etReq .Poison )
476
+ require .True (t , etReq .TxnHeartbeating )
477
+
478
+ br := ba .CreateReply ()
479
+ br .Txn = ba .Txn
480
+ br .Txn .Status = roachpb .ABORTED
481
+ return br , nil
482
+ },
483
+ )
484
+
485
+ // Spawn a goroutine to send the Put.
486
+ require .NoError (t , th .stopper .RunAsyncTask (ctx , "put" , func (ctx context.Context ) {
487
+ var ba roachpb.BatchRequest
488
+ ba .Header = roachpb.Header {Txn : txn .Clone ()}
489
+ ba .Add (& roachpb.PutRequest {RequestHeader : roachpb.RequestHeader {Key : roachpb .Key ("a" )}})
490
+
491
+ th .mu .Lock () // without TxnCoordSender, we must lock manually
492
+ defer th .mu .Unlock ()
493
+ br , pErr := th .SendLocked (ctx , ba )
494
+ require .Nil (t , pErr )
495
+ require .NotNil (t , br )
496
+ }))
497
+
498
+ <- putReady // wait for put
499
+ <- hbAborted // wait for heartbeat abort
500
+ select {
501
+ case <- rollbackSent : // we don't expect a rollback yet
502
+ require .Fail (t , "received unexpected EndTxn" )
503
+ case <- time .After (20 * time .Millisecond ):
504
+ }
505
+ close (putResume ) // make put return
506
+ <- rollbackSent // we now expect the rollback
507
+
508
+ // The heartbeat loop should eventually close.
509
+ waitForHeartbeatLoopToStop (t , & th )
510
+ }
511
+
512
+ // TestTxnHeartbeaterAsyncAbortCollapsesRequests tests that when the
513
+ // txnHeartbeater has an async abort rollback in flight, any client
514
+ // rollbacks will wait for the async rollback to complete and return
515
+ // its result.
516
+ func TestTxnHeartbeaterAsyncAbortCollapsesRequests (t * testing.T ) {
517
+ defer leaktest .AfterTest (t )()
518
+ defer log .Scope (t ).Close (t )
519
+ ctx := context .Background ()
520
+ txn := makeTxnProto ()
521
+ th , mockSender , mockGatekeeper := makeMockTxnHeartbeater (& txn )
522
+ defer th .stopper .Stop (ctx )
523
+
524
+ // Mock the heartbeat request, which simply aborts and signals hbAborted.
525
+ hbAborted := make (chan struct {})
526
+ mockGatekeeper .MockSend (func (ba roachpb.BatchRequest ) (* roachpb.BatchResponse , * roachpb.Error ) {
527
+ defer close (hbAborted )
528
+
529
+ require .Len (t , ba .Requests , 1 )
530
+ require .IsType (t , & roachpb.HeartbeatTxnRequest {}, ba .Requests [0 ].GetInner ())
531
+
532
+ br := ba .CreateReply ()
533
+ br .Txn = ba .Txn
534
+ br .Txn .Status = roachpb .ABORTED
535
+ return br , nil
536
+ })
537
+
538
+ // Mock an EndTxn response, which signals rollbackReady and blocks
539
+ // until rollbackUnblock is closed.
540
+ rollbackReady := make (chan struct {})
541
+ rollbackUnblock := make (chan struct {})
542
+ mockSender .ChainMockSend (
543
+ // The first Put request is expected and should just return.
544
+ func (ba roachpb.BatchRequest ) (* roachpb.BatchResponse , * roachpb.Error ) {
545
+ require .Len (t , ba .Requests , 1 )
546
+ require .IsType (t , & roachpb.PutRequest {}, ba .Requests [0 ].GetInner ())
547
+
548
+ br := ba .CreateReply ()
549
+ br .Txn = ba .Txn
550
+ return br , nil
551
+ },
552
+ // The first EndTxn request from the heartbeater is expected, so block and return.
553
+ func (ba roachpb.BatchRequest ) (* roachpb.BatchResponse , * roachpb.Error ) {
554
+ th .mu .Unlock () // manually unlock for concurrency, no txnLockGatekeeper
555
+ defer th .mu .Lock ()
556
+ close (rollbackReady )
557
+ require .Len (t , ba .Requests , 1 )
558
+ require .IsType (t , & roachpb.EndTxnRequest {}, ba .Requests [0 ].GetInner ())
559
+
560
+ <- rollbackUnblock
561
+
562
+ etReq := ba .Requests [0 ].GetInner ().(* roachpb.EndTxnRequest )
563
+ require .Equal (t , & txn , ba .Txn )
564
+ require .False (t , etReq .Commit )
565
+ require .True (t , etReq .Poison )
566
+ require .True (t , etReq .TxnHeartbeating )
567
+
568
+ br := ba .CreateReply ()
569
+ br .Txn = ba .Txn
570
+ br .Txn .Status = roachpb .ABORTED
571
+ return br , nil
572
+ },
573
+ // The second EndTxn request from the client is unexpected, so
574
+ // return an error response.
575
+ func (ba roachpb.BatchRequest ) (* roachpb.BatchResponse , * roachpb.Error ) {
576
+ return nil , roachpb .NewError (errors .Errorf ("unexpected request: %v" , ba ))
577
+ },
578
+ )
579
+
580
+ // Kick off the heartbeat loop.
581
+ var ba roachpb.BatchRequest
582
+ ba .Header = roachpb.Header {Txn : txn .Clone ()}
583
+ ba .Add (& roachpb.PutRequest {RequestHeader : roachpb.RequestHeader {Key : roachpb .Key ("a" )}})
584
+
585
+ th .mu .Lock () // manually lock, there's no TxnCoordSender
586
+ br , pErr := th .SendLocked (ctx , ba )
587
+ th .mu .Unlock ()
588
+ require .Nil (t , pErr )
589
+ require .NotNil (t , br )
590
+
591
+ // Wait for the heartbeater to abort and send an EndTxn.
592
+ <- hbAborted
593
+ <- rollbackReady
594
+
595
+ // Send a rollback from the client. This should be collapsed together
596
+ // with the heartbeat abort, and block until it returns. We spawn
597
+ // a goroutine to unblock the rollback.
598
+ require .NoError (t , th .stopper .RunAsyncTask (ctx , "put" , func (ctx context.Context ) {
599
+ time .Sleep (100 * time .Millisecond )
600
+ close (rollbackUnblock )
601
+ }))
602
+
603
+ ba = roachpb.BatchRequest {}
604
+ ba .Header = roachpb.Header {Txn : txn .Clone ()}
605
+ ba .Add (& roachpb.EndTxnRequest {Commit : false })
606
+
607
+ th .mu .Lock () // manually lock, there's no TxnCoordSender
608
+ br , pErr = th .SendLocked (ctx , ba )
609
+ th .mu .Unlock ()
610
+ require .Nil (t , pErr )
611
+ require .NotNil (t , br )
612
+
613
+ // The heartbeat loop should eventually close.
614
+ waitForHeartbeatLoopToStop (t , & th )
615
+ }
616
+
420
617
func heartbeaterRunning (th * txnHeartbeater ) bool {
421
618
th .mu .Lock ()
422
619
defer th .mu .Unlock ()
0 commit comments