Skip to content

Commit 0a8d10d

Browse files
authored
fix(datastore): detach rollback context from transaction cancellation (#14602)
Fix a lock contention and high-latency issue in RunInTransaction caused by failing to send Rollback commands when a context timer expires or is canceled. Fixes: [b/346474631](http://b/346474631)
1 parent d52fff4 commit 0a8d10d

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

datastore/transaction.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,21 @@ func (t *Transaction) Commit() (c *Commit, err error) {
475475
// Returns last attempt rollback error if rollback fails even after retries
476476
func (t *Transaction) rollbackWithRetry() error {
477477
var rollbackErr error
478+
// Use a detached context for rollback so that if the parent context is cancelled/timed out,
479+
// the rollback RPCs can still be sent to the server to release locks.
480+
// We set a timeout of 5 seconds on the rollback context.
481+
rollbackCtx, cancel := context.WithTimeout(context.WithoutCancel(t.ctx), 5*time.Second)
482+
defer cancel()
483+
484+
// Temporarily override the transaction context for the rollback operation.
485+
// Since Rollback and rollbackWithRetry are only called once at the end of a transaction,
486+
// this is clean and safe.
487+
origCtx := t.ctx
488+
t.ctx = rollbackCtx
489+
defer func() {
490+
t.ctx = origCtx
491+
}()
492+
478493
retryer := gax.OnCodes(rollbackRetryCodes, txnBackoff)
479494
for rollbackAttempt := 0; rollbackAttempt < maxIndividualReqTxnRetry; rollbackAttempt++ {
480495
rollbackErr = t.Rollback()

datastore/transaction_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,3 +881,30 @@ func TestRunInTransaction(t *testing.T) {
881881
})
882882
}
883883
}
884+
885+
func TestRunInTransaction_ContextCanceled(t *testing.T) {
886+
ctx, cancel := context.WithCancel(context.Background())
887+
client, srv, cleanup := newMock(t)
888+
defer cleanup()
889+
890+
srv.addRPC(&pb.BeginTransactionRequest{ProjectId: mockProjectID}, &pb.BeginTransactionResponse{Transaction: []byte("tid")})
891+
// We expect Rollback to be called even though context is canceled.
892+
srv.addRPC(&pb.RollbackRequest{ProjectId: mockProjectID, Transaction: []byte("tid")}, &pb.RollbackResponse{})
893+
894+
f := func(tx *Transaction) error {
895+
cancel() // Cancel the context inside the transaction function
896+
return nil
897+
}
898+
899+
_, err := client.RunInTransaction(ctx, f)
900+
if err == nil {
901+
t.Fatal("expected error, got nil")
902+
}
903+
if status.Code(err) != codes.Canceled && err != context.Canceled {
904+
t.Errorf("expected canceled error, got %v", err)
905+
}
906+
907+
if len(srv.reqItems) != 0 {
908+
t.Errorf("mock server has %d remaining expected RPCs (Rollback likely not called)", len(srv.reqItems))
909+
}
910+
}

0 commit comments

Comments
 (0)