WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
1 change: 0 additions & 1 deletion internal/integration/clam_prose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ func clamMultiByteTruncLogs(mt *mtest.T) []truncValidator {

// Insert started.
validators[0] = newTruncValidator(mt, cmd, func(cmd string) error {

// Remove the suffix from the command string.
cmd = cmd[:len(cmd)-len(logger.TruncationSuffix)]

Expand Down
36 changes: 32 additions & 4 deletions mongo/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,12 @@ type LabeledError interface {
HasErrorLabel(string) bool
}

type errorCoder interface {
ErrorCodes() []int
}

var _ errorCoder = ServerError(nil)

// ServerError is the interface implemented by errors returned from the server. Custom implementations of this
// interface should not be used in production.
type ServerError interface {
Expand Down Expand Up @@ -364,10 +370,12 @@ func hasErrorCode(srvErr ServerError, code int) bool {
return false
}

var _ ServerError = CommandError{}
var _ ServerError = WriteError{}
var _ ServerError = WriteException{}
var _ ServerError = BulkWriteException{}
var (
_ ServerError = CommandError{}
_ ServerError = WriteError{}
_ ServerError = WriteException{}
_ ServerError = BulkWriteException{}
)
Comment on lines +373 to +378
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to define interface{ ErrorCodes() []int } here and add additional checks that these structs also satisfies the interface? It may prevent ErrorCodes() in ServerError from being modified.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, will update


var _ error = ClientBulkWriteException{}

Expand Down Expand Up @@ -901,3 +909,23 @@ func joinBatchErrors(errs []error) string {

return buf.String()
}

// ErrorCodes returns the list of server error codes contained in err.
func ErrorCodes(err error) []int {
if err == nil {
return nil
}

var ec errorCoder
// First check if the error is already wrapped (common case)
if errors.As(err, &ec) {
return ec.ErrorCodes()
}

// Only wrap if necessary (for internal errors)
if errors.As(wrapErrors(err), &ec) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if wrapErrors is idempotent? I.e. if we double-wrap, does it hurt the correctness of ErrorCodes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapErrors is not idempotent in that calling it multiple times creates nested wrappers, but ErrorCodes is functionally idempotent because errors.As walks the entire error chain and finds the correct error code regardless of nesting depth. However, there is an inefficiency when ErrorCodes calls wrapErrors on already wrapped errors, creating unnecessary allocations. I'll add a commit to fix that 👍

return ec.ErrorCodes()
}

return []int{}
}
143 changes: 143 additions & 0 deletions mongo/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -760,3 +760,146 @@ func (n netErr) Temporary() bool {
}

var _ net.Error = (*netErr)(nil)

func TestErrorCodes(t *testing.T) {
tests := []struct {
name string
input error
want []int
}{
{
name: "nil error",
input: nil,
want: nil,
},
{
name: "non-server error",
input: errors.New("boom"),
want: []int{},
},
{
name: "CommandError single code",
input: CommandError{Code: 1},
want: []int{1},
},
{
name: "WriteError single code",
input: WriteError{Code: 1},
want: []int{1},
},
{
name: "WriteException write errors only",
input: WriteException{WriteErrors: WriteErrors{{Code: 1}, {Code: 2}}},
want: []int{1, 2},
},
{
name: "WriteException with write concern error",
input: WriteException{WriteErrors: WriteErrors{{Code: 1}}, WriteConcernError: &WriteConcernError{Code: 2}},
want: []int{1, 2},
},
{
name: "BulkWriteException write errors only",
input: BulkWriteException{
WriteErrors: []BulkWriteError{
{WriteError: WriteError{Code: 1}},
{WriteError: WriteError{Code: 2}},
},
},
want: []int{1, 2},
},
{
name: "BulkWriteException with write concern error",
input: BulkWriteException{
WriteErrors: []BulkWriteError{
{WriteError: WriteError{Code: 1}},
{WriteError: WriteError{Code: 2}},
},
WriteConcernError: &WriteConcernError{Code: 3},
},
want: []int{1, 2, 3},
},
{
name: "driver.Error wraps to CommandError",
input: driver.Error{Code: 1, Message: "shutdown in progress"},
want: []int{1},
},
{
name: "wrapped driver.Error",
input: fmt.Errorf("context: %w", driver.Error{Code: 1, Message: "ExceededTimeLimit"}),
want: []int{1},
},
{
input: wrapErrors(driver.Error{Code: 1, Message: "Custom error"}),
name: "double wrapped driver.Error",
want: []int{1},
},
{
name: "already wrapped CommandError",
input: CommandError{Code: 1},
want: []int{1},
},
{
name: "CommandError wrapped in fmt.Errorf",
input: fmt.Errorf("operation failed: %w", CommandError{Code: 1}),
want: []int{1},
},
{
name: "WriteException wrapped in fmt.Errorf",
input: fmt.Errorf("batch failed: %w", WriteException{
WriteErrors: WriteErrors{{Code: 1}, {Code: 2}},
}),
want: []int{1, 2},
},
{
name: "BulkWriteException with all error types",
input: BulkWriteException{
WriteErrors: []BulkWriteError{
{WriteError: WriteError{Code: 1}},
{WriteError: WriteError{Code: 2}},
{WriteError: WriteError{Code: 1}},
},
WriteConcernError: &WriteConcernError{Code: 2},
},
want: []int{1, 2, 1, 2},
},
{
name: "driver.Error with multiple fields",
input: driver.Error{Code: 1, Message: "test", Name: "TestError", Labels: []string{"label1"}},
want: []int{1},
},
{
name: "topology.ErrTopologyClosed converts to ErrClientDisconnected",
input: topology.ErrTopologyClosed,
want: []int{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.want, ErrorCodes(tt.input))
})
}
}

func TestErrorCodesNoDoubleWrapping(t *testing.T) {
driverErr := driver.Error{Code: 1, Message: "test error"}

// Wrap it once
wrapped := wrapErrors(driverErr)
cmdErr, ok := wrapped.(CommandError)
require.True(t, ok, "wrapErrors should return CommandError")
require.Equal(t, int32(1), cmdErr.Code)

// Call ErrorCodes on the wrapped error
codes := ErrorCodes(wrapped)
require.Equal(t, []int{1}, codes)

// The wrapped error's structure should not have changed
cmdErrAfter, ok := wrapped.(CommandError)
require.True(t, ok, "error should still be CommandError")
require.Equal(t, cmdErr.Code, cmdErrAfter.Code)

// Verify that calling ErrorCodes again gives same result
codes2 := ErrorCodes(wrapped)
require.Equal(t, codes, codes2)
}
1 change: 0 additions & 1 deletion x/mongo/driver/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,6 @@ func (op Operation) Execute(ctx context.Context) error {
var moreToCome bool
var startedInfo startedInformation
*wm, moreToCome, startedInfo, err = op.createWireMessage(ctx, maxTimeMS, (*wm)[:0], desc, conn, requestID)

if err != nil {
return err
}
Expand Down
Loading