Skip to content

Commit f057c85

Browse files
GODRIVER-3298 Handle joined errors correctly in WithTransaction. (#1928) [release/2.0] (#1931)
Co-authored-by: Matt Dale <[email protected]>
1 parent f327bc6 commit f057c85

File tree

3 files changed

+45
-31
lines changed

3 files changed

+45
-31
lines changed

mongo/errors.go

+2-17
Original file line numberDiff line numberDiff line change
@@ -166,25 +166,10 @@ func IsTimeout(err error) bool {
166166
return false
167167
}
168168

169-
// unwrap returns the inner error if err implements Unwrap(), otherwise it returns nil.
170-
func unwrap(err error) error {
171-
u, ok := err.(interface {
172-
Unwrap() error
173-
})
174-
if !ok {
175-
return nil
176-
}
177-
return u.Unwrap()
178-
}
179-
180169
// errorHasLabel returns true if err contains the specified label
181170
func errorHasLabel(err error, label string) bool {
182-
for ; err != nil; err = unwrap(err) {
183-
if le, ok := err.(LabeledError); ok && le.HasErrorLabel(label) {
184-
return true
185-
}
186-
}
187-
return false
171+
var le LabeledError
172+
return errors.As(err, &le) && le.HasErrorLabel(label)
188173
}
189174

190175
// IsNetworkError returns true if err is a network error

mongo/session.go

+17-14
Original file line numberDiff line numberDiff line change
@@ -107,20 +107,23 @@ func (s *Session) EndSession(ctx context.Context) {
107107
// parameter already has a Session attached to it, it will be replaced by this
108108
// session. The fn callback may be run multiple times during WithTransaction due
109109
// to retry attempts, so it must be idempotent.
110-
// If a command inside the callback fn fails, it may cause the transaction on the
111-
// server to be aborted. This situation is normally handled transparently by the
112-
// driver. However, if the application does not return that error from the fn,
113-
// the driver will not be able to determine whether the transaction was aborted or
114-
// not. The driver will then retry the block indefinitely.
115-
// To avoid this situation, the application MUST NOT silently handle errors within
116-
// the callback fn. If the application needs to handle errors within the block,
117-
// it MUST return them after doing so.
118-
// Non-retryable operation errors or any operation errors that occur after the timeout
119-
// expires will be returned without retrying. If the callback fails, the driver will call
120-
// AbortTransaction. Because this method must succeed to ensure that server-side
121-
// resources are properly cleaned up, context deadlines and cancellations will
122-
// not be respected during this call. For a usage example, see the
123-
// Client.StartSession method documentation.
110+
//
111+
// If a command inside the callback fn fails, it may cause the transaction on
112+
// the server to be aborted. This situation is normally handled transparently by
113+
// the driver. However, if the application does not return that error from the
114+
// fn, the driver will not be able to determine whether the transaction was
115+
// aborted or not. The driver will then retry the block indefinitely.
116+
//
117+
// To avoid this situation, the application MUST NOT silently handle errors
118+
// within the callback fn. If the application needs to handle errors within the
119+
// block, it MUST return them after doing so.
120+
//
121+
// Non-retryable operation errors or any operation errors that occur after the
122+
// timeout expires will be returned without retrying. If the callback fails, the
123+
// driver will call AbortTransaction. Because this method must succeed to ensure
124+
// that server-side resources are properly cleaned up, context deadlines and
125+
// cancellations will not be respected during this call. For a usage example,
126+
// see the Client.StartSession method documentation.
124127
func (s *Session) WithTransaction(
125128
ctx context.Context,
126129
fn func(ctx context.Context) (interface{}, error),

mongo/with_transactions_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"go.mongodb.org/mongo-driver/v2/event"
2222
"go.mongodb.org/mongo-driver/v2/internal/assert"
2323
"go.mongodb.org/mongo-driver/v2/internal/integtest"
24+
"go.mongodb.org/mongo-driver/v2/internal/require"
2425
"go.mongodb.org/mongo-driver/v2/mongo/options"
2526
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
2627
"go.mongodb.org/mongo-driver/v2/mongo/writeconcern"
@@ -576,6 +577,31 @@ func TestConvenientTransactions(t *testing.T) {
576577
"expected transaction to be passed within 2s")
577578

578579
})
580+
t.Run("retries correctly for joined errors", func(t *testing.T) {
581+
withTransactionTimeout = 500 * time.Millisecond
582+
583+
sess, err := client.StartSession()
584+
require.Nil(t, err, "StartSession error: %v", err)
585+
defer sess.EndSession(context.Background())
586+
587+
count := 0
588+
_, _ = sess.WithTransaction(context.Background(), func(context.Context) (interface{}, error) {
589+
count++
590+
time.Sleep(10 * time.Millisecond)
591+
592+
// Return a combined error value that is built using both
593+
// errors.Join and fmt.Errorf with multiple "%w" verbs, nesting a
594+
// retryable CommandError within the joined error tree.
595+
return nil, errors.Join(
596+
fmt.Errorf("%w, %w",
597+
CommandError{Name: "test err 1", Labels: []string{driver.TransientTransactionError}},
598+
errors.New("test err 2"),
599+
),
600+
errors.New("test err 3"),
601+
)
602+
})
603+
assert.Greater(t, count, 1, "expected WithTransaction callback to be retried at least once")
604+
})
579605
}
580606

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

0 commit comments

Comments
 (0)