Skip to content

Commit ed1fd57

Browse files
benjirewisBenjamin Rewis
authored andcommitted
GODRIVER-2001 Do not retry transaction after expired or canceled context (#668)
1 parent bd00e66 commit ed1fd57

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

mongo/session.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ func (s *sessionImpl) WithTransaction(ctx context.Context, fn func(sessCtx Sessi
193193
default:
194194
}
195195

196+
// End if context has timed out or been canceled, as retrying has no chance of success.
197+
if ctx.Err() != nil {
198+
return res, err
199+
}
196200
if errorHasLabel(err, driver.TransientTransactionError) {
197201
continue
198202
}

mongo/with_transactions_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,117 @@ func TestConvenientTransactions(t *testing.T) {
417417
assert.True(t, ok, "expected result type %T, got %T", false, res)
418418
assert.False(t, resBool, "expected result false, got %v", resBool)
419419
})
420+
t.Run("expired context before commitTransaction does not retry", func(t *testing.T) {
421+
withTransactionTimeout = 2 * time.Second
422+
423+
coll := db.Collection("test")
424+
// Explicitly create the collection on server because implicit collection creation is not allowed in
425+
// transactions for server versions <= 4.2.
426+
err := db.RunCommand(bgCtx, bson.D{{"create", coll.Name()}}).Err()
427+
assert.Nil(t, err, "error creating collection on server: %v", err)
428+
defer func() {
429+
_ = coll.Drop(bgCtx)
430+
}()
431+
432+
sess, err := client.StartSession()
433+
assert.Nil(t, err, "StartSession error: %v", err)
434+
defer sess.EndSession(context.Background())
435+
436+
// Create context with short timeout.
437+
withTransactionContext, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
438+
defer cancel()
439+
callback := func() {
440+
_, _ = sess.WithTransaction(withTransactionContext, func(sessCtx SessionContext) (interface{}, error) {
441+
_, err := coll.InsertOne(sessCtx, bson.D{{}})
442+
return nil, err
443+
})
444+
}
445+
446+
// Assert that transaction fails within 500ms and not 2 seconds.
447+
assert.Soon(t, callback, 500*time.Millisecond)
448+
})
449+
t.Run("canceled context before commitTransaction does not retry", func(t *testing.T) {
450+
withTransactionTimeout = 2 * time.Second
451+
452+
coll := db.Collection("test")
453+
// Explicitly create the collection on server because implicit collection creation is not allowed in
454+
// transactions for server versions <= 4.2.
455+
err := db.RunCommand(bgCtx, bson.D{{"create", coll.Name()}}).Err()
456+
assert.Nil(t, err, "error creating collection on server: %v", err)
457+
defer func() {
458+
_ = coll.Drop(bgCtx)
459+
}()
460+
461+
sess, err := client.StartSession()
462+
assert.Nil(t, err, "StartSession error: %v", err)
463+
defer sess.EndSession(context.Background())
464+
465+
// Create context and cancel it immediately.
466+
withTransactionContext, cancel := context.WithTimeout(context.Background(), 2*time.Second)
467+
cancel()
468+
callback := func() {
469+
_, _ = sess.WithTransaction(withTransactionContext, func(sessCtx SessionContext) (interface{}, error) {
470+
_, err := coll.InsertOne(sessCtx, bson.D{{}})
471+
return nil, err
472+
})
473+
}
474+
475+
// Assert that transaction fails within 500ms and not 2 seconds.
476+
assert.Soon(t, callback, 500*time.Millisecond)
477+
})
478+
t.Run("slow operation before commitTransaction retries", func(t *testing.T) {
479+
withTransactionTimeout = 2 * time.Second
480+
481+
coll := db.Collection("test")
482+
// Explicitly create the collection on server because implicit collection creation is not allowed in
483+
// transactions for server versions <= 4.2.
484+
err := db.RunCommand(bgCtx, bson.D{{"create", coll.Name()}}).Err()
485+
assert.Nil(t, err, "error creating collection on server: %v", err)
486+
defer func() {
487+
_ = coll.Drop(bgCtx)
488+
}()
489+
490+
// Set failpoint to block insertOne once for 500ms.
491+
failpoint := bson.D{{"configureFailPoint", "failCommand"},
492+
{"mode", bson.D{
493+
{"times", 1},
494+
}},
495+
{"data", bson.D{
496+
{"failCommands", bson.A{"insert"}},
497+
{"blockConnection", true},
498+
{"blockTimeMS", 500},
499+
}},
500+
}
501+
err = dbAdmin.RunCommand(bgCtx, failpoint).Err()
502+
assert.Nil(t, err, "error setting failpoint: %v", err)
503+
defer func() {
504+
err = dbAdmin.RunCommand(bgCtx, bson.D{
505+
{"configureFailPoint", "failCommand"},
506+
{"mode", "off"},
507+
}).Err()
508+
assert.Nil(t, err, "error turning off failpoint: %v", err)
509+
}()
510+
511+
sess, err := client.StartSession()
512+
assert.Nil(t, err, "StartSession error: %v", err)
513+
defer sess.EndSession(context.Background())
514+
515+
callback := func() {
516+
_, err = sess.WithTransaction(context.Background(), func(sessCtx SessionContext) (interface{}, error) {
517+
// Set a timeout of 300ms to cause a timeout on first insertOne
518+
// and force a retry.
519+
c, cancel := context.WithTimeout(sessCtx, 300*time.Millisecond)
520+
defer cancel()
521+
522+
_, err := coll.InsertOne(c, bson.D{{}})
523+
return nil, err
524+
})
525+
assert.Nil(t, err, "WithTransaction error: %v", err)
526+
}
527+
528+
// Assert that transaction passes within 2 seconds.
529+
assert.Soon(t, callback, 2*time.Second)
530+
})
420531
}
421532

422533
func setupConvenientTransactions(t *testing.T, extraClientOpts ...*options.ClientOptions) *Client {

0 commit comments

Comments
 (0)