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

Improving globalerror package #8110

Merged
merged 4 commits into from May 16, 2024
Merged

Improving globalerror package #8110

merged 4 commits into from May 16, 2024

Conversation

duricanikolic
Copy link
Contributor

What this PR does

This PR refactors the globalerror package in such a way that it exports the following functions:

  • ToGRPCStatusError(), that returns an error corresponding to a gRPC status built out of the given parameters: the gRPC status' code and details are passed as parameters, while its message corresponds to the original error. The resulting error is of type error, and it can be parsed to the corresponding gRPC status by both gogo/status and grpc/status packages.
  • WrapErrorWithGRPCStatus(), that wraps a given error with a gRPC status, which is built out of the given parameters: the gRPC status' code and details are passed as parameters, while its message corresponds to the original error. The resulting error is of type globalerror.ErrorWithStatus. This function replaces the previously available globalerror.NewErrorWithGRPCStatus() function.
  • WrapGRPCErrorWithContextError(), that checks if the given error is a gRPC error corresponding to a standard golang context error, and if it is, wraps the former with the latter. If the given error isn't a gRPC error, or it doesn't correspond to a standard golang context error, the original error is returned. This function replaces the previously available globalerror.WrapGrpcContextError() function.

This PR also replaces usages of globalerror.NewErrorWithGRPCStatus() in storegateway and querier packages with the new globalerror.ToGRPCStatusError(), because the latter is more appropriate.

Which issue(s) this PR fixes or relates to

Fixes #

Checklist

  • Tests updated.
  • Documentation added.
  • CHANGELOG.md updated - the order of entries should be [CHANGE], [FEATURE], [ENHANCEMENT], [BUGFIX].
  • about-versioning.md updated with experimental features.

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
Copy link
Contributor

@dimitarvdimitrov dimitarvdimitrov left a comment

Choose a reason for hiding this comment

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

generally I didn't see any blockers with the PR. But I'm wondering if we really need 2 different ways to wrap GRPC errors - ToGRPCStatusError and WrapErrorWithGRPCStatus. Wouldn't it be simpler if there was less choice around wrapping GRPC errors?

pkg/util/globalerror/grpc.go Outdated Show resolved Hide resolved
}

func (e ErrorWithStatus) Error() string {
func (e *ErrorWithStatus) Error() string {
Copy link
Contributor

Choose a reason for hiding this comment

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

pointer receivers on a method sometimes indicate that the type can be mutated by the method. Can't we get away with value receivers - (e ErrorWithStatus)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Initially I implemented it as you suggested. The problem is that with a value receiver ErrorWithStatus.Causes was not correctly passed form one method to another. That's why I decided to implement it this way.

But if you have other suggestions, I am happy to hear them.

This was the problem:

_, err := i.Push() // the result of i.Push()  was ingester.errPushGrpcDisabled
require.ErrorIs(t, err, errPushGrpcDisabled) // this was failing because err had the Causes field different from the actual one

Copy link
Contributor

Choose a reason for hiding this comment

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

When the values are pointers, then I think we hit this condition and the actual pointer addresses are compared instead of the errors https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/errors/wrap.go;l=55-57

This became a problem as Causes became a slice and slices make structs not comparable. Which makes me have second thoughts about having a slice in the struct. Maybe we can use fmt.Errorf("%w: %w", err, context.Canceled) in this place and avoid needing to store a slice ourselves. Or simply store two booleans in ErrorWithStatus indicating whether the error is a context cancelation or a deadline exceeded or neither.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @dimitarvdimitrov ,
I have removed pointer receivers, and returned ErrorWithStatus from every function that used to return *ErrorWithStatus. I have also removed the Causes []error slice, and put back the previous UnderlyingErr error, but added another field ctxErr err that gets assigned a value only after WrapGRPCErrorWithContextError () is called.

Similarly, Unwrap() []error returns []error{e.UnderlyingErr} or []error{UnderlyingErr, ctxErr} depending on whether ctxErr is set or not.

return e.Status.Message()
}

func (e ErrorWithStatus) Unwrap() error {
return e.UnderlyingErr
func (e *ErrorWithStatus) Unwrap() []error {
Copy link
Contributor

Choose a reason for hiding this comment

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

not very actionable, but decided to bring this up anyways

this changes the contract of this type. From what I could find implementing Unwrap() []error will still work with errors.Is() (code), but not with errors.Unwrap() (code). I think that should be fine because we don't use errors.Unwrap() in the Mimir repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @dimitarvdimitrov,
This is what the errors package documentation states:

An error e wraps another error if e's type has one of the methods

Unwrap() error
Unwrap() []error

So it is enough to implement one of these two in order to have correct error wrapping. Moreover, by doing it this way we can remove the Is() and As() functions that the grpcContextError type implemented.

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
@duricanikolic
Copy link
Contributor Author

generally I didn't see any blockers with the PR. But I'm wondering if we really need 2 different ways to wrap GRPC errors - ToGRPCStatusError and WrapErrorWithGRPCStatus. Wouldn't it be simpler if there was less choice around wrapping GRPC errors?

If this is the way we decide to go I can do it.
I opted for 2 separate methods because it is not always required to preserve the original error semantics. The status package itself doesn't preserve it, so I wanted to have both options, since both of them are actually needed in Mimir.

@dimitarvdimitrov
Copy link
Contributor

generally I didn't see any blockers with the PR. But I'm wondering if we really need 2 different ways to wrap GRPC errors - ToGRPCStatusError and WrapErrorWithGRPCStatus. Wouldn't it be simpler if there was less choice around wrapping GRPC errors?

If this is the way we decide to go I can do it. I opted for 2 separate methods because it is not always required to preserve the original error semantics. The status package itself doesn't preserve it, so I wanted to have both options, since both of them are actually needed in Mimir.

not feeling strongly about this. My consideration was for please new to the codebase or just everyone else who yet hasn't familiarized with error handling in the system. Are there drawbacks to always keeping the original error?

@duricanikolic
Copy link
Contributor Author

generally I didn't see any blockers with the PR. But I'm wondering if we really need 2 different ways to wrap GRPC errors - ToGRPCStatusError and WrapErrorWithGRPCStatus. Wouldn't it be simpler if there was less choice around wrapping GRPC errors?

If this is the way we decide to go I can do it. I opted for 2 separate methods because it is not always required to preserve the original error semantics. The status package itself doesn't preserve it, so I wanted to have both options, since both of them are actually needed in Mimir.

not feeling strongly about this. My consideration was for please new to the codebase or just everyone else who yet hasn't familiarized with error handling in the system. Are there drawbacks to always keeping the original error?

Hi @dimitarvdimitrov,
I have pushed another commit where I removed ToGRPCStatusError(), but I added the following function on ErrorWithStatus:

// Err returns an immutable error representing this ErrorWithStatus.
// Returns nil if UnderlyingError is nil or Status.Code() is OK.
// The resulting error is of type error, and it can be parsed to
// the corresponding gRPC status by both gogo/status and grpc/status
// packages.
func (e ErrorWithStatus) Err() error {
	if e.UnderlyingErr == nil {
		return nil
	}
	if e.Status.Code() == codes.OK {
		return nil
	}
	return e.Status.Err()
}

A similar function exists in both gogo and grpc status packages, and it is used to return an error corresponding to the status itself.

@@ -3289,15 +3289,15 @@ func TestShouldStopQueryFunc(t *testing.T) {
expected: false,
},
"should not stop query on store-gateway instance limit": {
err: globalerror.NewErrorWithGRPCStatus(errors.New("instance limit"), codes.Aborted, &mimirpb.ErrorDetails{Cause: mimirpb.INSTANCE_LIMIT}),
err: globalerror.WrapErrorWithGRPCStatus(errors.New("instance limit"), codes.Aborted, &mimirpb.ErrorDetails{Cause: mimirpb.INSTANCE_LIMIT}).Err(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

.Err() is not mandatory

@@ -709,9 +709,9 @@ func mapSeriesError(err error) error {
case errors.As(err, &stGwErr):
switch cause := stGwErr.errorCause(); cause {
case mimirpb.INSTANCE_LIMIT:
return globalerror.NewErrorWithGRPCStatus(stGwErr, codes.Unavailable, &mimirpb.ErrorDetails{Cause: cause})
return globalerror.WrapErrorWithGRPCStatus(stGwErr, codes.Unavailable, &mimirpb.ErrorDetails{Cause: cause}).Err()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

.Err() is not mandatory, it is just used to remove semantic connection with stGwErr

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
Copy link
Contributor

@dimitarvdimitrov dimitarvdimitrov left a comment

Choose a reason for hiding this comment

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

LGTM

@duricanikolic duricanikolic marked this pull request as ready for review May 16, 2024 11:01
@duricanikolic duricanikolic requested a review from a team as a code owner May 16, 2024 11:01
@duricanikolic duricanikolic merged commit aea91a4 into main May 16, 2024
29 checks passed
@duricanikolic duricanikolic deleted the yuri/errors branch May 16, 2024 11:01
francoposa pushed a commit that referenced this pull request May 27, 2024
* Improving globalerror package

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

* Make ingester tests pass

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

* Get rid of createCauses()

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

* Improve ErrorWithStatus struct

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>

---------

Signed-off-by: Yuri Nikolic <durica.nikolic@grafana.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants