Skip to content

Commit ce5f4db

Browse files
authored
feat(internal): provide wrapping for retried errors (#4797)
This modifies the retry code to allow both context and service errors to be available when the call was retried until hitting a context deadline or cancelation. This preserves the error string as it is currently put together by internal/annotate; for example a googleapi error from storage looks like this: `object.Attrs: retry failed with context deadline exceeded; last error: googleapi: got HTTP response code 503 with body: {"code":"503","message":{"error":{"message":"Retry Test: Caused a 503"}}}` Go 1.13 error semantics can be used to introspect the underlying context and service errors; here are some examples: ``` _, err = client.Bucket(bucketName).Object(objName).Attrs(timeoutCtx) if err != nil { if e, ok := err.(interface{ Unwrap() error }); ok { wrappedErr := e.Unwrap() // yields googleapi.Error type. } // errors.As allows unwrapping with access to googleapi.Error fields. var errAsGoogleapi *googleapi.Error if errors.As(err, &errAsGoogleapi) { log.Printf("error code: %v", errAsGoogleapi.Code) } isDeadlineExceeded := errors.Is(err, context.DeadlineExceeded) // true } ``` internal.Retry is used by the storage, datastore and bigquery clients, as well as integration tests for compute and pubsub. I want to make sure to check this with everyone affected.
1 parent 797a9bd commit ce5f4db

File tree

2 files changed

+42
-5
lines changed

2 files changed

+42
-5
lines changed

internal/retry.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ package internal
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"time"
2021

2122
gax "github.com/googleapis/gax-go/v2"
23+
"google.golang.org/grpc/status"
2224
)
2325

2426
// Retry calls the supplied function f repeatedly according to the provided
@@ -44,11 +46,40 @@ func retry(ctx context.Context, bo gax.Backoff, f func() (stop bool, err error),
4446
lastErr = err
4547
}
4648
p := bo.Pause()
47-
if cerr := sleep(ctx, p); cerr != nil {
49+
if ctxErr := sleep(ctx, p); ctxErr != nil {
4850
if lastErr != nil {
49-
return Annotatef(lastErr, "retry failed with %v; last error", cerr)
51+
return wrappedCallErr{ctxErr: ctxErr, wrappedErr: lastErr}
5052
}
51-
return cerr
53+
return ctxErr
5254
}
5355
}
5456
}
57+
58+
// Use this error type to return an error which allows introspection of both
59+
// the context error and the error from the service.
60+
type wrappedCallErr struct {
61+
ctxErr error
62+
wrappedErr error
63+
}
64+
65+
func (e wrappedCallErr) Error() string {
66+
return fmt.Sprintf("retry failed with %v; last error: %v", e.ctxErr, e.wrappedErr)
67+
}
68+
69+
func (e wrappedCallErr) Unwrap() error {
70+
return e.wrappedErr
71+
}
72+
73+
// Is allows errors.Is to match the error from the call as well as context
74+
// sentinel errors.
75+
func (e wrappedCallErr) Is(err error) bool {
76+
return e.ctxErr == err || e.wrappedErr == err
77+
}
78+
79+
// GRPCStatus allows the wrapped error to be used with status.FromError.
80+
func (e wrappedCallErr) GRPCStatus() *status.Status {
81+
if s, ok := status.FromError(e.wrappedErr); ok {
82+
return s
83+
}
84+
return nil
85+
}

internal/retry_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package internal
1717
import (
1818
"context"
1919
"errors"
20-
"fmt"
2120
"testing"
2221
"time"
2322

@@ -75,14 +74,21 @@ func TestRetryPreserveError(t *testing.T) {
7574
func(context.Context, time.Duration) error {
7675
return context.DeadlineExceeded
7776
})
77+
if err == nil {
78+
t.Fatalf("unexpectedly got nil error")
79+
}
80+
wantError := "retry failed with context deadline exceeded; last error: rpc error: code = NotFound desc = not found"
81+
if g, w := err.Error(), wantError; g != w {
82+
t.Errorf("got error %q, want %q", g, w)
83+
}
7884
got, ok := status.FromError(err)
7985
if !ok {
8086
t.Fatalf("got %T, wanted a status", got)
8187
}
8288
if g, w := got.Code(), codes.NotFound; g != w {
8389
t.Errorf("got code %v, want %v", g, w)
8490
}
85-
wantMessage := fmt.Sprintf("retry failed with %v; last error: not found", context.DeadlineExceeded)
91+
wantMessage := "not found"
8692
if g, w := got.Message(), wantMessage; g != w {
8793
t.Errorf("got message %q, want %q", g, w)
8894
}

0 commit comments

Comments
 (0)