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

feat(internal): provide wrapping for retried errors #4797

Merged
merged 9 commits into from Sep 23, 2021
33 changes: 32 additions & 1 deletion internal/retry.go
Expand Up @@ -16,9 +16,11 @@ package internal

import (
"context"
"fmt"
"time"

gax "github.com/googleapis/gax-go/v2"
"google.golang.org/grpc/status"
)

// Retry calls the supplied function f repeatedly according to the provided
Expand Down Expand Up @@ -46,9 +48,38 @@ func retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error),
p := bo.Pause()
if cerr := sleep(ctx, p); cerr != nil {
if lastErr != nil {
return Annotatef(lastErr, "retry failed with %v; last error", cerr)
return wrappedCallErr{cerr: cerr, wrappedErr: lastErr}
}
return cerr
}
}
}

// Use this error type to return an error which allows introspection of both
// the context error and the error from the service.
type wrappedCallErr struct {
cerr error
tritone marked this conversation as resolved.
Show resolved Hide resolved
wrappedErr error
}

func (e wrappedCallErr) Error() string {
return fmt.Sprintf("retry failed with %v; last error: %v", e.cerr, e.wrappedErr)
}

func (e wrappedCallErr) Unwrap() error {
return e.wrappedErr
}

// Is allows errors.Is to match the error from the call as well as context
// sentinel errors.
func (e wrappedCallErr) Is(err error) bool {
return e.cerr == err || e.wrappedErr == err
}

// GRPCStatus allows the wrapped error to be used with status.FromError.
func (e wrappedCallErr) GRPCStatus() *status.Status {
if s, ok := status.FromError(e.wrappedErr); ok {
return s
}
return nil
}
10 changes: 8 additions & 2 deletions internal/retry_test.go
Expand Up @@ -17,7 +17,6 @@ package internal
import (
"context"
"errors"
"fmt"
"testing"
"time"

Expand Down Expand Up @@ -75,14 +74,21 @@ func TestRetryPreserveError(t *testing.T) {
func(context.Context, time.Duration) error {
return context.DeadlineExceeded
})
if err == nil {
t.Fatalf("unexpectedly got nil error")
}
wantError := "retry failed with context deadline exceeded; last error: rpc error: code = NotFound desc = not found"
if g, w := err.Error(), wantError; g != w {
t.Errorf("got error %q, want %q", g, w)
}
got, ok := status.FromError(err)
if !ok {
t.Fatalf("got %T, wanted a status", got)
}
if g, w := got.Code(), codes.NotFound; g != w {
t.Errorf("got code %v, want %v", g, w)
}
wantMessage := fmt.Sprintf("retry failed with %v; last error: not found", context.DeadlineExceeded)
wantMessage := "not found"
if g, w := got.Message(), wantMessage; g != w {
t.Errorf("got message %q, want %q", g, w)
}
Expand Down