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

retry: adds ability to return metadata for each request attempt #1031

Merged
merged 10 commits into from
Jan 14, 2021
116 changes: 85 additions & 31 deletions aws/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,23 @@ func (r ClientRequestID) HandleBuild(ctx context.Context, in middleware.BuildInp
return next.HandleBuild(ctx, in)
}

// AttemptClockSkew calculates the clock skew of the SDK client
// TODO: Could be a better name, since this calculates more then skew
type AttemptClockSkew struct{}
// RecordResponseTiming records the response timing for the SDK client requests.
type RecordResponseTiming struct{}

// ID is the middleware identifier
func (a *AttemptClockSkew) ID() string {
return "AttemptClockSkew"
func (a *RecordResponseTiming) ID() string {
return "RecordResponseTiming"
}

// HandleDeserialize calculates response metadata and clock skew
func (a AttemptClockSkew) HandleDeserialize(ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) (
func (a RecordResponseTiming) HandleDeserialize(ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) (
out middleware.DeserializeOutput, metadata middleware.Metadata, err error,
) {
respMeta := ResponseMetadata{}

out, metadata, err = next.HandleDeserialize(ctx, in)
respMeta.ResponseAt = sdk.NowTime()
responseAt := sdk.NowTime()
setResponseAt(&metadata, responseAt)

var serverTime time.Time

switch resp := out.RawResponse.(type) {
case *smithyhttp.Response:
Expand All @@ -67,48 +67,102 @@ func (a AttemptClockSkew) HandleDeserialize(ctx context.Context, in middleware.D
break
}
var parseErr error
respMeta.ServerTime, parseErr = http.ParseTime(respDateHeader)
serverTime, parseErr = http.ParseTime(respDateHeader)
if parseErr != nil {
// TODO: What should logging of errors look like?
logger := middleware.GetLogger(ctx)
logger.Logf("failed to parse response Date Header value, got %v",
parseErr.Error())
break
}
setServerTime(&metadata, serverTime)
}

if !respMeta.ServerTime.IsZero() {
respMeta.AttemptSkew = respMeta.ServerTime.Sub(respMeta.ResponseAt)
if !serverTime.IsZero() {
attemptSkew := serverTime.Sub(responseAt)
setAttemptSkew(&metadata, attemptSkew)
}

setResponseMetadata(&metadata, respMeta)

return out, metadata, err
}

type responseMetadataKey struct{}
type responseAtKey struct{}

// ResponseMetadata is metadata about the transport layer response
type ResponseMetadata struct {
ResponseAt time.Time
ServerTime time.Time
AttemptSkew time.Duration
// GetResponseAt returns the time response was received at.
func GetResponseAt(metadata middleware.Metadata) (v time.Time, ok bool) {
v, ok = metadata.Get(responseAtKey{}).(time.Time)
return v, ok
}

// GetResponseMetadata retrieves response metadata from the context, if nil returns an empty value
func GetResponseMetadata(metadata middleware.Metadata) (v ResponseMetadata) {
v, _ = metadata.Get(responseMetadataKey{}).(ResponseMetadata)
return v
// setResponseAt sets the response time on the metadata.
func setResponseAt(metadata *middleware.Metadata, v time.Time) {
metadata.Set(responseAtKey{}, v)
}

// setResponseMetadata sets the ResponseMetadata on the given context
func setResponseMetadata(metadata *middleware.Metadata, responseMetadata ResponseMetadata) {
metadata.Set(responseMetadataKey{}, responseMetadata)
type serverTimeKey struct{}

// GetServerTime returns the server time for response.
func GetServerTime(metadata middleware.Metadata) (v time.Time, ok bool) {
v, ok = metadata.Get(serverTimeKey{}).(time.Time)
return v, ok
}

// setServerTime sets the server time on the metadata.
func setServerTime(metadata *middleware.Metadata, v time.Time) {
metadata.Set(serverTimeKey{}, v)
}

type attemptSkewKey struct{}

// GetAttemptSkew returns Attempt clock skew for response from metadata.
func GetAttemptSkew(metadata middleware.Metadata) (v time.Duration, ok bool) {
v, ok = metadata.Get(attemptSkewKey{}).(time.Duration)
return v, ok
}

// setAttemptSkew sets the attempt clock skew on the metadata.
func setAttemptSkew(metadata *middleware.Metadata, v time.Duration) {
metadata.Set(attemptSkewKey{}, v)
}

// AddClientRequestIDMiddleware adds ClientRequestID to the middleware stack
func AddClientRequestIDMiddleware(stack *middleware.Stack) error {
return stack.Build.Add(&ClientRequestID{}, middleware.After)
}

// AddAttemptClockSkewMiddleware adds AttemptClockSkew to the middleware stack
func AddAttemptClockSkewMiddleware(stack *middleware.Stack) error {
return stack.Deserialize.Add(&AttemptClockSkew{}, middleware.After)
// AddRecordResponseTiming adds RecordResponseTiming middleware to the
// middleware stack.
func AddRecordResponseTiming(stack *middleware.Stack) error {
return stack.Deserialize.Add(&RecordResponseTiming{}, middleware.After)
}

// rawResponseKey is the accessor key used to store and access the
// raw response within the response metadata.
type rawResponseKey struct{}

// addRawResponse middleware adds raw response on to the metadata
type addRawResponse struct{}

// ID the identifier for the ClientRequestID
func (m *addRawResponse) ID() string {
return "AddRawResponseToMetadata"
}

// HandleDeserialize adds raw response on the middleware metadata
func (m addRawResponse) HandleDeserialize(ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) (
out middleware.DeserializeOutput, metadata middleware.Metadata, err error,
) {
out, metadata, err = next.HandleDeserialize(ctx, in)
metadata.Set(rawResponseKey{}, out.RawResponse)
return out, metadata, err
}

// AddRawResponseToMetadata adds middleware to the middleware stack that
// store raw response on to the metadata.
func AddRawResponseToMetadata(stack *middleware.Stack) error {
return stack.Deserialize.Add(&addRawResponse{}, middleware.After)
}
skotambkar marked this conversation as resolved.
Show resolved Hide resolved

// GetRawResponse returns raw response set on metadata
func GetRawResponse(metadata middleware.Metadata) interface{} {
return metadata.Get(rawResponseKey{})
}
59 changes: 36 additions & 23 deletions aws/middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ func TestClientRequestID(t *testing.T) {

func TestAttemptClockSkewHandler(t *testing.T) {
cases := map[string]struct {
Next smithymiddleware.DeserializeHandlerFunc
Expect middleware.ResponseMetadata
ResponseAt func() time.Time
Next smithymiddleware.DeserializeHandlerFunc
ResponseAt func() time.Time
ExpectAttemptSkew time.Duration
ExpectServerTime time.Time
ExpectResponseAt time.Time
}{
"no response": {
Next: func(ctx context.Context, in smithymiddleware.DeserializeInput,
Expand All @@ -70,9 +72,7 @@ func TestAttemptClockSkewHandler(t *testing.T) {
ResponseAt: func() time.Time {
return time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
},
Expect: middleware.ResponseMetadata{
ResponseAt: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC),
},
ExpectResponseAt: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC),
},
"failed response": {
Next: func(ctx context.Context, in smithymiddleware.DeserializeInput,
Expand All @@ -88,9 +88,7 @@ func TestAttemptClockSkewHandler(t *testing.T) {
ResponseAt: func() time.Time {
return time.Date(2020, 6, 7, 8, 9, 10, 0, time.UTC)
},
Expect: middleware.ResponseMetadata{
ResponseAt: time.Date(2020, 6, 7, 8, 9, 10, 0, time.UTC),
},
ExpectResponseAt: time.Date(2020, 6, 7, 8, 9, 10, 0, time.UTC),
},
"no date header response": {
Next: func(ctx context.Context, in smithymiddleware.DeserializeInput,
Expand All @@ -106,9 +104,7 @@ func TestAttemptClockSkewHandler(t *testing.T) {
ResponseAt: func() time.Time {
return time.Date(2020, 11, 12, 13, 14, 15, 0, time.UTC)
},
Expect: middleware.ResponseMetadata{
ResponseAt: time.Date(2020, 11, 12, 13, 14, 15, 0, time.UTC),
},
ExpectResponseAt: time.Date(2020, 11, 12, 13, 14, 15, 0, time.UTC),
},
"invalid date header response": {
Next: func(ctx context.Context, in smithymiddleware.DeserializeInput,
Expand All @@ -126,9 +122,7 @@ func TestAttemptClockSkewHandler(t *testing.T) {
ResponseAt: func() time.Time {
return time.Date(2020, 1, 2, 16, 17, 18, 0, time.UTC)
},
Expect: middleware.ResponseMetadata{
ResponseAt: time.Date(2020, 1, 2, 16, 17, 18, 0, time.UTC),
},
ExpectResponseAt: time.Date(2020, 1, 2, 16, 17, 18, 0, time.UTC),
},
"date response": {
Next: func(ctx context.Context, in smithymiddleware.DeserializeInput,
Expand All @@ -146,11 +140,9 @@ func TestAttemptClockSkewHandler(t *testing.T) {
ResponseAt: func() time.Time {
return time.Date(2020, 3, 5, 22, 25, 17, 0, time.UTC)
},
Expect: middleware.ResponseMetadata{
ResponseAt: time.Date(2020, 3, 5, 22, 25, 17, 0, time.UTC),
ServerTime: time.Date(2020, 3, 5, 22, 25, 15, 0, time.UTC),
AttemptSkew: -2 * time.Second,
},
ExpectResponseAt: time.Date(2020, 3, 5, 22, 25, 17, 0, time.UTC),
ExpectServerTime: time.Date(2020, 3, 5, 22, 25, 15, 0, time.UTC),
ExpectAttemptSkew: -2 * time.Second,
},
}

Expand All @@ -163,13 +155,34 @@ func TestAttemptClockSkewHandler(t *testing.T) {
}()
sdk.NowTime = c.ResponseAt
}
mw := middleware.AttemptClockSkew{}
mw := middleware.RecordResponseTiming{}
_, metadata, err := mw.HandleDeserialize(context.Background(), smithymiddleware.DeserializeInput{}, c.Next)
if err != nil {
t.Errorf("expect no error, got %v", err)
}
if e, a := c.Expect, middleware.GetResponseMetadata(metadata); !reflect.DeepEqual(e, a) {
t.Errorf("expect %v, got %v", e, a)

if v, ok := middleware.GetResponseAt(metadata); ok {
if !reflect.DeepEqual(v, c.ExpectResponseAt) {
t.Fatalf("expected %v, got %v", c.ExpectResponseAt, v)
}
} else if !c.ExpectResponseAt.IsZero() {
t.Fatal("expected response at to be set in metadata, was not")
}

if v, ok := middleware.GetServerTime(metadata); ok {
if !reflect.DeepEqual(v, c.ExpectServerTime) {
t.Fatalf("expected %v, got %v", c.ExpectServerTime, v)
}
} else if !c.ExpectServerTime.IsZero() {
t.Fatal("expected server time to be set in metadata, was not")
}

if v, ok := middleware.GetAttemptSkew(metadata); ok {
if !reflect.DeepEqual(v, c.ExpectAttemptSkew) {
t.Fatalf("expected %v, got %v", c.ExpectAttemptSkew, v)
}
} else if c.ExpectAttemptSkew != 0 {
t.Fatal("expected attempt skew to be set in metadata, was not")
}
})
}
Expand Down
52 changes: 52 additions & 0 deletions aws/retry/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package retry

import (
awsmiddle "github.com/aws/aws-sdk-go-v2/aws/middleware"
"github.com/aws/smithy-go/middleware"
)

// attemptResultsKey is a metadata accessor key to retrieve metadata
// for all request attempts.
type attemptResultsKey struct {
}

// GetAttemptResults retrieves attempts results from middleware metadata.
func GetAttemptResults(metadata middleware.Metadata) (AttemptResults, bool) {
m, ok := metadata.Get(attemptResultsKey{}).(AttemptResults)
return m, ok
}

// AttemptResults represents struct containing metadata returned by all request attempts.
type AttemptResults struct {

// Results is a slice consisting attempt result from all request attempts.
// Results are stored in order request attempt is made.
Results []AttemptResult
}

// AttemptResult represents attempt result returned by a single request attempt.
type AttemptResult struct {

// Err is the error if received for the request attempt.
Err error

// Retryable denotes if request may be retried. This states if an
// error is considered retryable.
Retryable bool

// Retried indicates if this request was retried.
Retried bool

// ResponseMetadata is any existing metadata passed via the response middlewares.
ResponseMetadata middleware.Metadata
}

// addAttemptResults adds attempt results to middleware metadata
func addAttemptResults(metadata *middleware.Metadata, v AttemptResults) {
metadata.Set(attemptResultsKey{}, v)
}

// GetRawResponse returns raw response recorded for the attempt result
func (a AttemptResult) GetRawResponse() interface{} {
return awsmiddle.GetRawResponse(a.ResponseMetadata)
}
Loading