-
Notifications
You must be signed in to change notification settings - Fork 70
errorbase: optimize errors.Is #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+246
−77
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| package errors_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net" | ||
| "testing" | ||
|
|
||
| "github.com/cockroachdb/errors" | ||
| ) | ||
|
|
||
| func BenchmarkErrorsIs(b *testing.B) { | ||
| b.Run("NilError", func(b *testing.B) { | ||
| var err error | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("SimpleError", func(b *testing.B) { | ||
| err := errors.New("test") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("WrappedError", func(b *testing.B) { | ||
| baseErr := errors.New("test") | ||
| err := errors.Wrap(baseErr, "wrapped error") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("WrappedWithStack", func(b *testing.B) { | ||
| baseErr := errors.New("test") | ||
| err := errors.WithStack(baseErr) | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("NetworkError", func(b *testing.B) { | ||
| netErr := &net.OpError{ | ||
| Op: "dial", | ||
| Net: "tcp", | ||
| Addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 26257}, | ||
| Err: fmt.Errorf("connection refused"), | ||
| } | ||
| err := errors.Wrap(netErr, "network connection failed") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("DeeplyWrappedNetworkError", func(b *testing.B) { | ||
| netErr := &net.OpError{ | ||
| Op: "dial", | ||
| Net: "tcp", | ||
| Addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 26257}, | ||
| Err: fmt.Errorf("connection refused"), | ||
| } | ||
| err := errors.WithStack(netErr) | ||
| err = errors.Wrap(err, "failed to connect to database") | ||
| err = errors.Wrap(err, "unable to establish connection") | ||
| err = errors.WithStack(err) | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("MultipleWrappedErrors", func(b *testing.B) { | ||
| baseErr := errors.New("internal error") | ||
| err := errors.WithStack(baseErr) | ||
| err = errors.Wrap(err, "operation failed") | ||
| err = errors.WithStack(err) | ||
| err = errors.Wrap(err, "transaction failed") | ||
| err = errors.WithStack(err) | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("NetworkErrorWithLongAddress", func(b *testing.B) { | ||
| netErr := &net.OpError{ | ||
| Op: "read", | ||
| Net: "tcp", | ||
| Addr: &net.TCPAddr{ | ||
| IP: net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), | ||
| Port: 26257, | ||
| }, | ||
| Err: fmt.Errorf("i/o timeout"), | ||
| } | ||
| err := errors.WithStack(netErr) | ||
| err = errors.Wrap(err, "failed to read from connection") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("WithMessage", func(b *testing.B) { | ||
| baseErr := errors.New("test") | ||
| err := errors.WithMessage(baseErr, "additional context") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("MultipleWithMessage", func(b *testing.B) { | ||
| baseErr := errors.New("internal error") | ||
| err := errors.WithMessage(baseErr, "first message") | ||
| err = errors.WithMessage(err, "second message") | ||
| err = errors.WithMessage(err, "third message") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("WithMessageAndStack", func(b *testing.B) { | ||
| baseErr := errors.New("test") | ||
| err := errors.WithStack(baseErr) | ||
| err = errors.WithMessage(err, "operation context") | ||
| err = errors.WithStack(err) | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("NetworkErrorWithMessage", func(b *testing.B) { | ||
| netErr := &net.OpError{ | ||
| Op: "dial", | ||
| Net: "tcp", | ||
| Addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 26257}, | ||
| Err: fmt.Errorf("connection refused"), | ||
| } | ||
| err := errors.WithMessage(netErr, "database connection failed") | ||
| err = errors.WithMessage(err, "unable to reach server") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("NetworkErrorWithEverything", func(b *testing.B) { | ||
| netErr := &net.OpError{ | ||
| Op: "dial", | ||
| Net: "tcp", | ||
| Addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 26257}, | ||
| Err: fmt.Errorf("connection refused"), | ||
| } | ||
| err := errors.WithStack(netErr) | ||
| err = errors.WithMessage(err, "database connection failed") | ||
| err = errors.Wrap(err, "failed to establish TCP connection") | ||
| err = errors.WithStack(err) | ||
| err = errors.WithMessage(err, "unable to reach CockroachDB server") | ||
| err = errors.Wrap(err, "connection attempt failed") | ||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
|
|
||
| b.Run("DeeplyNested100Levels", func(b *testing.B) { | ||
| baseErr := errors.New("base error") | ||
| err := baseErr | ||
|
|
||
| // Create a 100-level deep error chain | ||
| for i := 0; i < 100; i++ { | ||
| switch i % 3 { | ||
| case 0: | ||
| err = errors.Wrap(err, fmt.Sprintf("wrap level %d", i)) | ||
| case 1: | ||
| err = errors.WithMessage(err, fmt.Sprintf("message level %d", i)) | ||
| case 2: | ||
| err = errors.WithStack(err) | ||
| } | ||
| } | ||
|
|
||
| for range b.N { | ||
| errors.Is(err, context.Canceled) | ||
| } | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,29 +68,42 @@ func Is(err, reference error) bool { | |
| } | ||
| } | ||
|
|
||
| if err == nil { | ||
| // Err is nil and reference is non-nil, so it cannot match. We | ||
| // want to short-circuit the loop below in this case, otherwise | ||
| // we're paying the expense of getMark() without need. | ||
| return false | ||
| } | ||
|
|
||
| // Not directly equal. Try harder, using error marks. We don't do | ||
| // this during the loop above as it may be more expensive. | ||
| // | ||
| // Note: there is a more effective recursive algorithm that ensures | ||
| // that any pair of string only gets compared once. Should the | ||
| // following code become a performance bottleneck, that algorithm | ||
| // can be considered instead. | ||
| refMark := getMark(reference) | ||
| for c := err; c != nil; c = errbase.UnwrapOnce(c) { | ||
| if equalMarks(getMark(c), refMark) { | ||
| for errNext := err; errNext != nil; errNext = errbase.UnwrapOnce(errNext) { | ||
| if isMarkEqual(errNext, reference) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| return false | ||
| } | ||
|
|
||
| func isMarkEqual(err, reference error) bool { | ||
| _, errIsMark := err.(*withMark) | ||
| _, refIsMark := reference.(*withMark) | ||
| if errIsMark || refIsMark { | ||
| // If either error is a mark, use the more general | ||
| // equalMarks() function. | ||
| return equalMarks(getMark(err), getMark(reference)) | ||
| } | ||
|
|
||
| m1 := err | ||
| m2 := reference | ||
| for m1 != nil && m2 != nil { | ||
| if !errbase.EqualTypeMark(m1, m2) { | ||
| return false | ||
| } | ||
| m1 = errbase.UnwrapOnce(m1) | ||
| m2 = errbase.UnwrapOnce(m2) | ||
| } | ||
|
|
||
| // The two chains have different lengths, so they cannot be equal. | ||
| if m1 != nil || m2 != nil { | ||
| return false | ||
| } | ||
|
|
||
| return safeGetErrMsg(err) == safeGetErrMsg(reference) | ||
| } | ||
|
|
||
| func tryDelegateToIsMethod(err, reference error) bool { | ||
| if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(reference) { | ||
| return true | ||
|
|
@@ -150,62 +163,9 @@ func If(err error, pred func(err error) (interface{}, bool)) (interface{}, bool) | |
| // package location or a different type, ensure that | ||
| // RegisterTypeMigration() was called prior to IsAny(). | ||
| func IsAny(err error, references ...error) bool { | ||
| if err == nil { | ||
| for _, refErr := range references { | ||
| if refErr == nil { | ||
| return true | ||
| } | ||
| } | ||
| // The mark-based comparison below will never match anything if | ||
| // the error is nil, so don't bother with computing the marks in | ||
| // that case. This avoids the computational expense of computing | ||
| // the reference marks upfront. | ||
| return false | ||
| } | ||
|
|
||
| // First try using direct reference comparison. | ||
| for c := err; c != nil; c = errbase.UnwrapOnce(c) { | ||
| for _, refErr := range references { | ||
| if refErr == nil { | ||
| continue | ||
| } | ||
| isComparable := reflect.TypeOf(refErr).Comparable() | ||
| if isComparable && c == refErr { | ||
| return true | ||
| } | ||
| // Compatibility with std go errors: if the error object itself | ||
| // implements Is(), try to use that. | ||
| if tryDelegateToIsMethod(c, refErr) { | ||
| return true | ||
| } | ||
| } | ||
|
|
||
| // Recursively try multi-error causes, if applicable. | ||
| for _, me := range errbase.UnwrapMulti(c) { | ||
| if IsAny(me, references...) { | ||
| return true | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Try harder with marks. | ||
| // Note: there is a more effective recursive algorithm that ensures | ||
| // that any pair of string only gets compared once. Should this | ||
| // become a performance bottleneck, that algorithm can be considered | ||
| // instead. | ||
| refMarks := make([]errorMark, 0, len(references)) | ||
| for _, refErr := range references { | ||
| if refErr == nil { | ||
| continue | ||
| } | ||
| refMarks = append(refMarks, getMark(refErr)) | ||
| } | ||
| for c := err; c != nil; c = errbase.UnwrapOnce(c) { | ||
| errMark := getMark(c) | ||
| for _, refMark := range refMarks { | ||
| if equalMarks(errMark, refMark) { | ||
| return true | ||
| } | ||
| for _, reference := range references { | ||
| if Is(err, reference) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
|
|
@@ -221,6 +181,9 @@ func equalMarks(m1, m2 errorMark) bool { | |
| if m1.msg != m2.msg { | ||
| return false | ||
| } | ||
| if len(m1.types) != len(m2.types) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bug that is pretty unlikely to occur in the original implementation that checks error message before types, but its somewhat likely now that we check the types first. |
||
| return false | ||
| } | ||
| for i, t := range m1.types { | ||
| if !t.Equals(m2.types[i]) { | ||
| return false | ||
|
|
@@ -234,7 +197,10 @@ func getMark(err error) errorMark { | |
| if m, ok := err.(*withMark); ok { | ||
| return m.mark | ||
| } | ||
| m := errorMark{msg: safeGetErrMsg(err), types: []errorspb.ErrorTypeMark{errbase.GetTypeMark(err)}} | ||
| m := errorMark{ | ||
| msg: safeGetErrMsg(err), | ||
| types: []errorspb.ErrorTypeMark{errbase.GetTypeMark(err)}, | ||
| } | ||
| for c := errbase.UnwrapOnce(err); c != nil; c = errbase.UnwrapOnce(c) { | ||
| m.types = append(m.types, errbase.GetTypeMark(c)) | ||
| } | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the reason we didn't use this implementation originally is
Iswas pretty slow. So it was better to use the quick check before the slow mark check. Now thatIsis efficient, we don't need to duplicate the implementations.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the consolidation.