Skip to content

Commit 2855941

Browse files
benjirewisBenji Rewis
authored andcommitted
GODRIVER-2565 Abort transaction before CommitLoop if context errored. (#1101)
1 parent 3f59691 commit 2855941

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

mongo/session.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,27 @@ func (s *sessionImpl) WithTransaction(ctx context.Context, fn func(sessCtx Sessi
206206
return res, err
207207
}
208208

209+
// Check if callback intentionally aborted and, if so, return immediately
210+
// with no error.
209211
err = s.clientSession.CheckAbortTransaction()
210212
if err != nil {
211213
return res, nil
212214
}
213215

216+
// If context has errored, run AbortTransaction and return, as the CommitLoop
217+
// has no chance of succeeding.
218+
//
219+
// Aborting after a failed CommitTransaction is dangerous. Failed transaction
220+
// commits may unpin the session server-side, and subsequent transaction aborts
221+
// may run on a new mongos which could end up with commit and abort being executed
222+
// simultaneously.
223+
if ctx.Err() != nil {
224+
// Wrap the user-provided Context in a new one that behaves like context.Background() for deadlines and
225+
// cancellations, but forwards Value requests to the original one.
226+
_ = s.AbortTransaction(internal.NewBackgroundContext(ctx))
227+
return nil, ctx.Err()
228+
}
229+
214230
CommitLoop:
215231
for {
216232
err = s.CommitTransaction(ctx)

mongo/with_transactions_test.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -348,9 +348,36 @@ func TestConvenientTransactions(t *testing.T) {
348348
return nil
349349
})
350350
})
351-
t.Run("commitTransaction timeout does not retry", func(t *testing.T) {
351+
t.Run("context error before commitTransaction does not retry and aborts", func(t *testing.T) {
352352
withTransactionTimeout = 2 * time.Second
353353

354+
// Create a special CommandMonitor that only records information about abortTransaction events.
355+
var abortStarted []*event.CommandStartedEvent
356+
var abortSucceeded []*event.CommandSucceededEvent
357+
var abortFailed []*event.CommandFailedEvent
358+
monitor := &event.CommandMonitor{
359+
Started: func(ctx context.Context, evt *event.CommandStartedEvent) {
360+
if evt.CommandName == "abortTransaction" {
361+
abortStarted = append(abortStarted, evt)
362+
}
363+
},
364+
Succeeded: func(_ context.Context, evt *event.CommandSucceededEvent) {
365+
if evt.CommandName == "abortTransaction" {
366+
abortSucceeded = append(abortSucceeded, evt)
367+
}
368+
},
369+
Failed: func(_ context.Context, evt *event.CommandFailedEvent) {
370+
if evt.CommandName == "abortTransaction" {
371+
abortFailed = append(abortFailed, evt)
372+
}
373+
},
374+
}
375+
376+
// Set up a new Client using the command monitor defined above get a handle to a collection. The collection
377+
// needs to be explicitly created on the server because implicit collection creation is not allowed in
378+
// transactions for server versions <= 4.2.
379+
client := setupConvenientTransactions(t, options.Client().SetMonitor(monitor))
380+
db := client.Database("foo")
354381
coll := db.Collection("test")
355382
// Explicitly create the collection on server because implicit collection creation is not allowed in
356383
// transactions for server versions <= 4.2.
@@ -377,7 +404,8 @@ func TestConvenientTransactions(t *testing.T) {
377404
}
378405
}()
379406

380-
// Insert a document within a session and manually cancel context.
407+
// Insert a document within a session and manually cancel context before
408+
// "commitTransaction" can be sent.
381409
callback := func(ctx context.Context) {
382410
transactionCtx, cancel := context.WithCancel(ctx)
383411

@@ -391,6 +419,12 @@ func TestConvenientTransactions(t *testing.T) {
391419

392420
// Assert that transaction is canceled within 500ms and not 2 seconds.
393421
assert.Soon(t, callback, 500*time.Millisecond)
422+
423+
// Assert that AbortTransaction was started once and succeeded.
424+
assert.Equal(t, 1, len(abortStarted), "expected 1 abortTransaction started event, got %d", len(abortStarted))
425+
assert.Equal(t, 1, len(abortSucceeded), "expected 1 abortTransaction succeeded event, got %d",
426+
len(abortSucceeded))
427+
assert.Equal(t, 0, len(abortFailed), "expected 0 abortTransaction failed events, got %d", len(abortFailed))
394428
})
395429
t.Run("wrapped transient transaction error retried", func(t *testing.T) {
396430
sess, err := client.StartSession()
@@ -416,7 +450,7 @@ func TestConvenientTransactions(t *testing.T) {
416450
assert.True(t, ok, "expected result type %T, got %T", false, res)
417451
assert.False(t, resBool, "expected result false, got %v", resBool)
418452
})
419-
t.Run("expired context before commitTransaction does not retry", func(t *testing.T) {
453+
t.Run("expired context before callback does not retry", func(t *testing.T) {
420454
withTransactionTimeout = 2 * time.Second
421455

422456
coll := db.Collection("test")
@@ -446,7 +480,7 @@ func TestConvenientTransactions(t *testing.T) {
446480
// Assert that transaction fails within 500ms and not 2 seconds.
447481
assert.Soon(t, callback, 500*time.Millisecond)
448482
})
449-
t.Run("canceled context before commitTransaction does not retry", func(t *testing.T) {
483+
t.Run("canceled context before callback does not retry", func(t *testing.T) {
450484
withTransactionTimeout = 2 * time.Second
451485

452486
coll := db.Collection("test")
@@ -476,7 +510,7 @@ func TestConvenientTransactions(t *testing.T) {
476510
// Assert that transaction fails within 500ms and not 2 seconds.
477511
assert.Soon(t, callback, 500*time.Millisecond)
478512
})
479-
t.Run("slow operation before commitTransaction retries", func(t *testing.T) {
513+
t.Run("slow operation in callback retries", func(t *testing.T) {
480514
withTransactionTimeout = 2 * time.Second
481515

482516
coll := db.Collection("test")

0 commit comments

Comments
 (0)