Skip to content
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

status: Implement *statusError.Is #2868

Merged
merged 1 commit into from
Jul 24, 2019
Merged

status: Implement *statusError.Is #2868

merged 1 commit into from
Jul 24, 2019

Conversation

jsm
Copy link
Contributor

@jsm jsm commented Jun 13, 2019

In the go2 error proposal the Is function adds matching of error targets.
On this line of the proposal implementation, there is an interface check for Is, to provide
custom implementation of this matching.

gRPC status errors that are defined as a var, currently don't successfully work
with this function when crossing the gRPC boundary, as the pointer values will
not be equivalent.

Ideally, the following should work across gRPC boundaries:

var ErrAuthInvalidToken = status.Error(codes.Unauthenticated, "Invalid token")
_, err := grpcClient.Call(...) // Returns the above
errors.Is(err, ErrAuthInvalidToken) == true

@thelinuxfoundation
Copy link

Thank you for your pull request. Before we can look at your contribution, we need to ensure all contributors are covered by a Contributor License Agreement.

After the following items are addressed, please respond with a new comment here, and the automated system will re-verify.

Regards,
CLA GitHub bot

@dfawley dfawley self-requested a review June 20, 2019 20:22
@dfawley dfawley self-assigned this Jun 20, 2019
Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

Thanks for the PR, I agree this will be useful to have.

The CLAs still seem to be unhappy - can you take another look at that, please?

status/status.go Outdated
return false
}

return se.Code == tse.Code && se.Message == tse.Message
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't check the Details field -- to do so, I believe you can use proto.Equal(se, tse) instead of comparing the fields individually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dfawley I believe we don't actually want to check the Details field, as we very well might have different details added onto the same error "type"

I imagine WithDetails could be used for things like tagging something like the userId, or host source, which would break the equivalency.

Copy link
Member

Choose a reason for hiding this comment

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

The same types of arguments could be used for checking the Message field as well. It's common for users to put debugging details (username, raw request information, etc) into RPC error strings. Whether this is more or less likely is conjecture. I believe the spirit of "Is" is to perform an exact match, not a fuzzy one. But I'll have to look at the errors proposal again.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking about this some more, I think since Details is just appended to the end of the message, equality would break if they were added in a different order, which doesn't seem great.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm still of the opinion that it should be matching on just the code and message, but I'm happy to do a strict comparison instead if that will help push this through.

Copy link
Member

Choose a reason for hiding this comment

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

For As, I'm unsure about implementing it on &status.Status, as this isn't actually an error type, and it might be an anti-pattern to have As work on a non-error type? Maybe they intend for it to be usable this way but it seems a little weird.

I agree this seems a bit weird. As would need to be implemented on *statusError, but it would assign to a *status.Status. I'm not sure if this would be considered an anti-pattern, but I will pose the question.

Alternatively, we could export a interface{ GRPCStatus() *status.Status } type to be used in As.

I considered this as well. I'm not sure this option would be intuitive or discoverable, and I'd rather just offer a status.Unwrap(error) helper instead.

For the convenience wrapper, it seems like it would make sense to implement it in FromError(), although that seems like it would have to be in a separate go2 package.

We would have to move FromError into its own file, and define two versions, one for pre-1.13 versions of Go and one for 1.13+. It might be undesirable/unexpected to unwrap in that function automatically, although I'm having trouble seeing where it would cause problems to do so.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure if this would be considered an anti-pattern, but I will pose the question.

Did you get a chance to think about this more?

Your plan for FromError seems good.

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I have not had a chance to ask yet. I'll see if I can find something out soon.

Copy link

Choose a reason for hiding this comment

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

errors.Is() compares every single element in a chain of errors by Unwrap()ing the next level.
WithDetails() just adds such an element to the chain, so why should it be part of the comparison?

It just uses a different way for adding an element than the error package, but the principle is the same.

Copy link
Member

Choose a reason for hiding this comment

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

That's an interesting thought, but the problem is that WithDetails isn't necessarily wrapping a different error - it can also explain more about the error itself. We can't simply ignore it in a function intended to test strict equality.

@dfawley dfawley assigned jsm and unassigned dfawley Jun 21, 2019
@jsm
Copy link
Contributor Author

jsm commented Jun 21, 2019

@dfawley Working on getting a CCLA pushed through!

@jsm
Copy link
Contributor Author

jsm commented Jun 26, 2019

@dfawley CLA got signed, thanks for your patience.

@jsm
Copy link
Contributor Author

jsm commented Jul 9, 2019

@dfawley I updated this PR to use proto.Equal

@dfawley dfawley assigned dfawley and unassigned jsm Jul 11, 2019
@jsm
Copy link
Contributor Author

jsm commented Jul 23, 2019

@dfawley Friendly bump :)

status/status.go Outdated
// Is implements future error.Is functionality.
// A statusError is equivalent if the code and message are identical.
func (se *statusError) Is(target error) bool {
if target == nil {
Copy link
Member

Choose a reason for hiding this comment

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

Minor point: a nil target will also make the following type assertion fail, so this condition is unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

testErrWithDetails := statusWithDetails.Err()

// Test cases.
testCases := []testCaseErrorIs{
Copy link
Member

Choose a reason for hiding this comment

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

This type can be declared inline to avoid putting it in the global namespace:

testCases := []struct {
  err1, err2 error
  want bool
}{
...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was necessary for the appends, but I fixed that!

func TestErrorIs(t *testing.T) {
// Test errors.
testErr := status.Error(codes.Internal, "internal server error")
statusWithDetails, _ := status.New(codes.Internal, "internal server error").WithDetails(&grpc_testing.Empty{})
Copy link
Member

@dfawley dfawley Jul 23, 2019

Choose a reason for hiding this comment

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

I'd recommend a helper:

func errWithDetails(t *testing.T, s *Status, details ...proto.Message) error {
  t.Helper()
  res, err := s.WithDetails(details)
  if err != nil {
    t.Fatalf("(%v).WithDetails(%v) = %v, %v; want _, <nil>", s, details, res, err)
  }
  return res
}

...

  testErrWithDetails := errWithDetails(status.New(codes.Internal, "internal server error"), &grpc_testing.Empty{})

This will also make it so you don't need to append any test cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea

status/status.go Outdated
return false
}

return se.Code == tse.Code && se.Message == tse.Message
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I have not had a chance to ask yet. I'll see if I can find something out soon.

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

Please also add a grpc copyright notice to the top of status/status_ext_test.go - or merge it into status_test.go. (This is what is causing travis to fail.)

@jsm
Copy link
Contributor Author

jsm commented Jul 24, 2019

@dfawley Updated!

I need to keep status_ext_test.go separate, as I needed it to be outside of the status package (it's package is status_test) to use the grpc_testing package without cycles.

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

Looks good, but please change the copyright year. Thanks!


is := isError.Is(tc.err2)
if is != tc.want {
t.Errorf("(%v).Is(%v) = %t; want %t", tc.err1, tc.err2, is, tc.want)
Copy link
Member

Choose a reason for hiding this comment

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

TIL %t

@@ -0,0 +1,72 @@
/*
*
* Copyright 2017 gRPC authors.
Copy link
Member

Choose a reason for hiding this comment

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

2019 please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

In the go2 error proposal the Is function adds matching of error targets.
On this line: https://github.com/golang/exp/blob/sumdb/v0.0.2/errors/wrap.go#L58
of the proposal implementation, there is an interface check for Is, to provide
custom implementation of this matching.

gRPC status errors that are defined as a var, currently don't successfully work
with this function when crossing the gRPC boundary, as the pointer values will
not be equivalent.

Ideally, the following should work across gRPC boundaries:
var ErrAuthInvalidToken = status.Error(codes.Unauthenticated, "Invalid token")
_, err := grpcClient.Call(...) // Returns the above
err.Is(ErrAuthInvalidToken) == true
@dfawley dfawley added the Type: Feature New features or improvements in behavior label Jul 24, 2019
@dfawley dfawley added this to the 1.23 Release milestone Jul 24, 2019
@dfawley dfawley merged commit 61f27c1 into grpc:master Jul 24, 2019
@jsm
Copy link
Contributor Author

jsm commented Jul 24, 2019

🎉 Let me know if you'd like me to work with you on the As implementation.

@dfawley
Copy link
Member

dfawley commented Jul 24, 2019

🎉 Let me know if you'd like me to work with you on the As implementation.

Thanks for the offer. I filed an issue for this and CC'd you on it.

Edit: #2934

srenatus added a commit to chef/automate that referenced this pull request Aug 14, 2019
### Release notes

- https://github.com/grpc/grpc-go/releases/tag/v1.23.0
  - much stuff! a bunch of HTTP2-related fixes -- we don't _expose_ go-grpc's listener, but we use it interally all over the place
  - there's `*satus.Is` now: grpc/grpc-go#2868 -- we could make some use of that, I suppose.
  - a bunch of perf improvements, always welcome :)
- https://github.com/golang/protobuf/releases/tag/v1.3.2
  - small fry, but nice stuff: golang/protobuf#785 adds an Unimplemented method to each server, allowing old servers to be used when there's proto changes.

* deps: bump protobuf (1.3.2) and go-grpc (1.23.0)
* deps: bump protobuf (1.3.2) and go-grpc (1.23.0) [revendor]
* deps: bump protobuf (1.3.2) and go-grpc (1.23.0) [regenerate]

Signed-off-by: Stephan Renatus <[email protected]>
@lock lock bot locked as resolved and limited conversation to collaborators Feb 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Type: Feature New features or improvements in behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants