Skip to content

Commit 0f1fe1b

Browse files
authored
aws/defaults: Add recording clock skew metadata per request attempt (#500)
Updates the SDK's default request handlers to record metadata about the clock skew from a request attempt. This metadata will be used by the SDK's request retry metadata header. This update does NOT add clock skew correction to the SDK's request signatures.
1 parent 2b617db commit 0f1fe1b

File tree

6 files changed

+237
-12
lines changed

6 files changed

+237
-12
lines changed

aws/defaults/defaults.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func Handlers() aws.Handlers {
7676
handlers.Send.PushFrontNamed(RetryMetricHeaderHandler)
7777
handlers.Send.PushBackNamed(ValidateReqSigHandler)
7878
handlers.Send.PushBackNamed(SendHandler)
79+
handlers.Send.PushBackNamed(AttemptClockSkewHandler)
7980
handlers.ShouldRetry.PushBackNamed(RetryableCheckHandler)
8081
handlers.ValidateResponse.PushBackNamed(ValidateResponseHandler)
8182

aws/defaults/handlers.go

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"github.com/aws/aws-sdk-go-v2/aws"
1616
"github.com/aws/aws-sdk-go-v2/aws/awserr"
17+
"github.com/aws/aws-sdk-go-v2/internal/sdk"
1718
)
1819

1920
// Interface for matching types which also have a Len method.
@@ -122,6 +123,7 @@ var SendHandler = aws.NamedHandler{
122123

123124
var err error
124125
r.HTTPResponse, err = r.Config.HTTPClient.Do(r.HTTPRequest)
126+
r.ResponseAt = sdk.NowTime()
125127
if err != nil {
126128
handleSendError(r, err)
127129
}
@@ -204,12 +206,32 @@ var RetryMetricHeaderHandler = aws.NamedHandler{
204206
if max := r.Retryer.MaxAttempts(); max != 0 {
205207
parts = append(parts, fmt.Sprintf("max=%d", max))
206208
}
207-
// TODO "ttl=YYYYmmddTHHMMSSZ"
208-
// ttl = current_time + socket_read_timeout + estimated_skew
209-
// SDK doesn't implement clock skew yet and not obvious how to obtain
210-
// read deadlines.
211209

212-
r.HTTPRequest.Header.Set("amz-sdk-request", strings.Join(parts, "; "))
210+
type timeoutGetter interface {
211+
GetTimeout() time.Duration
212+
}
213+
214+
var ttl time.Time
215+
// Attempt extract the TTL from context deadline, or timeout on the client.
216+
if v, ok := r.Config.HTTPClient.(timeoutGetter); ok {
217+
if t := v.GetTimeout(); t > 0 {
218+
ttl = sdk.NowTime().Add(t)
219+
}
220+
}
221+
if ttl.IsZero() {
222+
if deadline, ok := r.Context().Deadline(); ok {
223+
ttl = deadline
224+
}
225+
}
226+
227+
// Only append the TTL if it can be determined.
228+
if !ttl.IsZero() && len(r.AttemptClockSkews) > 0 {
229+
const unixTimeFormat = "20060102T150405Z"
230+
ttl = ttl.Add(r.AttemptClockSkews[len(r.AttemptClockSkews)-1])
231+
parts = append(parts, fmt.Sprintf("ttl=%s", ttl.Format(unixTimeFormat)))
232+
}
233+
234+
r.HTTPRequest.Header.Set(retryMetricHeader, strings.Join(parts, "; "))
213235
}}
214236

215237
// RetryableCheckHandler performs final checks to determine if the request should
@@ -252,3 +274,34 @@ var ValidateEndpointHandler = aws.NamedHandler{Name: "core.ValidateEndpointHandl
252274
r.Error = aws.ErrMissingEndpoint
253275
}
254276
}}
277+
278+
// AttemptClockSkewHandler records the estimated clock skew between the client
279+
// and service response clocks. This estimation will be no more granular than
280+
// one second. It will not be populated until after at least the first
281+
// attempt's response is received.
282+
var AttemptClockSkewHandler = aws.NamedHandler{
283+
Name: "core.AttemptClockSkewHandler",
284+
Fn: func(r *aws.Request) {
285+
if r.ResponseAt.IsZero() || r.HTTPResponse == nil || r.HTTPResponse.StatusCode == 0 {
286+
return
287+
}
288+
289+
respDateHeader := r.HTTPResponse.Header.Get("Date")
290+
if len(respDateHeader) == 0 {
291+
return
292+
}
293+
294+
respDate, err := http.ParseTime(respDateHeader)
295+
if err != nil {
296+
if r.Config.Logger != nil {
297+
r.Config.Logger.Log(fmt.Sprintf("ERROR: unable to determine clock skew for %s/%s API response, invalid Date header value, %v",
298+
r.Metadata.ServiceName, r.Operation.Name, respDateHeader))
299+
}
300+
return
301+
}
302+
303+
r.AttemptClockSkews = append(r.AttemptClockSkews,
304+
respDate.Sub(r.ResponseAt),
305+
)
306+
},
307+
}

aws/defaults/handlers_test.go

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/aws/aws-sdk-go-v2/aws/retry"
1919
"github.com/aws/aws-sdk-go-v2/internal/awstesting"
2020
"github.com/aws/aws-sdk-go-v2/internal/awstesting/unit"
21+
"github.com/aws/aws-sdk-go-v2/internal/sdk"
2122
"github.com/aws/aws-sdk-go-v2/service/s3"
2223
)
2324

@@ -349,10 +350,21 @@ func TestRequestInvocationIDHeaderHandler(t *testing.T) {
349350
}
350351

351352
func TestRetryMetricHeaderHandler(t *testing.T) {
353+
nowTime := sdk.NowTime
354+
defer func() {
355+
sdk.NowTime = nowTime
356+
}()
357+
sdk.NowTime = func() time.Time {
358+
return time.Date(2020, 2, 2, 0, 0, 0, 0, time.UTC)
359+
}
360+
352361
cases := map[string]struct {
353-
Attempt int
354-
MaxAttempts int
355-
Expect string
362+
Attempt int
363+
MaxAttempts int
364+
Client aws.HTTPClient
365+
ContextDeadline time.Time
366+
AttemptClockSkews []time.Duration
367+
Expect string
356368
}{
357369
"first attempt": {
358370
Attempt: 1, MaxAttempts: 3,
@@ -366,15 +378,43 @@ func TestRetryMetricHeaderHandler(t *testing.T) {
366378
Attempt: 10,
367379
Expect: "attempt=10",
368380
},
381+
"with ttl client timeout": {
382+
Attempt: 2, MaxAttempts: 3,
383+
AttemptClockSkews: []time.Duration{
384+
10 * time.Second,
385+
},
386+
Client: func() aws.HTTPClient {
387+
c := &aws.BuildableHTTPClient{}
388+
return c.WithTimeout(10 * time.Second)
389+
}(),
390+
Expect: "attempt=2; max=3; ttl=20200202T000020Z",
391+
},
392+
"with ttl context deadline": {
393+
Attempt: 1, MaxAttempts: 3,
394+
AttemptClockSkews: []time.Duration{
395+
10 * time.Second,
396+
},
397+
ContextDeadline: sdk.NowTime().Add(10 * time.Second),
398+
Expect: "attempt=1; max=3; ttl=20200202T000020Z",
399+
},
369400
}
370401

371402
for name, c := range cases {
372403
t.Run(name, func(t *testing.T) {
373404
cfg := unit.Config()
405+
if c.Client != nil {
406+
cfg.HTTPClient = c.Client
407+
}
374408
r := aws.New(cfg, aws.Metadata{}, cfg.Handlers, aws.NoOpRetryer{},
375409
&aws.Operation{}, &struct{}{}, struct{}{})
410+
if !c.ContextDeadline.IsZero() {
411+
ctx, cancel := context.WithDeadline(r.Context(), c.ContextDeadline)
412+
defer cancel()
413+
r.SetContext(ctx)
414+
}
376415

377416
r.AttemptNum = c.Attempt
417+
r.AttemptClockSkews = c.AttemptClockSkews
378418
r.Retryer = retry.AddWithMaxAttempts(r.Retryer, c.MaxAttempts)
379419

380420
defaults.RetryMetricHeaderHandler.Fn(r)
@@ -388,3 +428,92 @@ func TestRetryMetricHeaderHandler(t *testing.T) {
388428
})
389429
}
390430
}
431+
432+
func TestAttemptClockSkewHandler(t *testing.T) {
433+
cases := map[string]struct {
434+
Req *aws.Request
435+
Expect []time.Duration
436+
}{
437+
"no response": {Req: &aws.Request{}},
438+
"failed response": {
439+
Req: &aws.Request{
440+
HTTPResponse: &http.Response{StatusCode: 0, Header: http.Header{}},
441+
},
442+
},
443+
"no date header response": {
444+
Req: &aws.Request{
445+
HTTPResponse: &http.Response{StatusCode: 200, Header: http.Header{}},
446+
},
447+
},
448+
"invalid date header response": {
449+
Req: &aws.Request{
450+
HTTPResponse: &http.Response{
451+
StatusCode: 200,
452+
Header: http.Header{"Date": []string{"abc123"}},
453+
},
454+
},
455+
},
456+
"response at unset": {
457+
Req: &aws.Request{
458+
HTTPResponse: &http.Response{
459+
StatusCode: 200,
460+
Header: http.Header{
461+
"Date": []string{"Thu, 05 Mar 2020 22:25:15 GMT"},
462+
},
463+
},
464+
},
465+
},
466+
"first date response": {
467+
Req: &aws.Request{
468+
HTTPResponse: &http.Response{
469+
StatusCode: 200,
470+
Header: http.Header{
471+
"Date": []string{"Thu, 05 Mar 2020 22:25:15 GMT"},
472+
},
473+
},
474+
ResponseAt: time.Date(2020, 3, 5, 22, 25, 17, 0, time.UTC),
475+
},
476+
Expect: []time.Duration{-2 * time.Second},
477+
},
478+
"subsequent date response": {
479+
Req: &aws.Request{
480+
HTTPResponse: &http.Response{
481+
StatusCode: 200,
482+
Header: http.Header{
483+
"Date": []string{"Thu, 05 Mar 2020 22:25:15 GMT"},
484+
},
485+
},
486+
ResponseAt: time.Date(2020, 3, 5, 22, 25, 14, 0, time.UTC),
487+
AttemptClockSkews: []time.Duration{
488+
-2 * time.Second,
489+
},
490+
},
491+
Expect: []time.Duration{
492+
-2 * time.Second,
493+
1 * time.Second,
494+
},
495+
},
496+
}
497+
498+
for name, c := range cases {
499+
t.Run(name, func(t *testing.T) {
500+
r := new(aws.Request)
501+
*r = *c.Req
502+
503+
defaults.AttemptClockSkewHandler.Fn(r)
504+
if err := r.Error; err != nil {
505+
t.Fatalf("expect no error, got %v", err)
506+
}
507+
508+
if e, a := len(c.Expect), len(r.AttemptClockSkews); e != a {
509+
t.Errorf("expect %v skews, got %v", e, a)
510+
}
511+
512+
for i, s := range r.AttemptClockSkews {
513+
if e, a := c.Expect[i], s; e != a {
514+
t.Errorf("%d, expect %v skew, got %v", i, e, a)
515+
}
516+
}
517+
})
518+
}
519+
}

aws/http_client.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ type BuildableHTTPClient struct {
4444
dialer *net.Dialer
4545

4646
initOnce *sync.Once
47-
client *http.Client
47+
48+
clientTimeout time.Duration
49+
client *http.Client
4850
}
4951

5052
// NewBuildableHTTPClient returns an initialized client for invoking HTTP
@@ -91,11 +93,12 @@ func (b BuildableHTTPClient) build() *http.Client {
9193
http2.ConfigureTransport(tr)
9294

9395
return wrapWithoutRedirect(&http.Client{
96+
Timeout: b.clientTimeout,
9497
Transport: tr,
9598
})
9699
}
97100

98-
func (b BuildableHTTPClient) reset() BuildableHTTPClient {
101+
func (b BuildableHTTPClient) initReset() BuildableHTTPClient {
99102
b.initOnce = new(sync.Once)
100103
b.client = nil
101104
return b
@@ -108,7 +111,7 @@ func (b BuildableHTTPClient) reset() BuildableHTTPClient {
108111
// will be replaced with a default Transport value before invoking the option
109112
// functions.
110113
func (b BuildableHTTPClient) WithTransportOptions(opts ...func(*http.Transport)) HTTPClient {
111-
b = b.reset()
114+
b = b.initReset()
112115

113116
tr := b.GetTransport()
114117
for _, opt := range opts {
@@ -123,7 +126,7 @@ func (b BuildableHTTPClient) WithTransportOptions(opts ...func(*http.Transport))
123126
// net.Dialer options applied. Will set the client's http.Transport DialContext
124127
// member.
125128
func (b BuildableHTTPClient) WithDialerOptions(opts ...func(*net.Dialer)) HTTPClient {
126-
b = b.reset()
129+
b = b.initReset()
127130

128131
dialer := b.GetDialer()
129132
for _, opt := range opts {
@@ -138,6 +141,15 @@ func (b BuildableHTTPClient) WithDialerOptions(opts ...func(*net.Dialer)) HTTPCl
138141
return &b
139142
}
140143

144+
// WithTimeout Sets the timeout used by the client for all requests.
145+
func (b BuildableHTTPClient) WithTimeout(timeout time.Duration) HTTPClient {
146+
b = b.initReset()
147+
148+
b.clientTimeout = timeout
149+
150+
return &b
151+
}
152+
141153
// GetTransport returns a copy of the client's HTTP Transport.
142154
func (b BuildableHTTPClient) GetTransport() *http.Transport {
143155
var tr *http.Transport
@@ -162,6 +174,11 @@ func (b BuildableHTTPClient) GetDialer() *net.Dialer {
162174
return dialer
163175
}
164176

177+
// GetTimeout returns a copy of the client's timeout to cancel requests with.
178+
func (b BuildableHTTPClient) GetTimeout() time.Duration {
179+
return b.clientTimeout
180+
}
181+
165182
func defaultDialer() *net.Dialer {
166183
return &net.Dialer{
167184
Timeout: DefaultDialConnectTimeout,

aws/http_client_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55
"net/http/httptest"
66
"testing"
7+
"time"
78

89
"github.com/aws/aws-sdk-go-v2/aws"
910
)
@@ -27,3 +28,18 @@ func TestBuildableHTTPClient_NoFollowRedirect(t *testing.T) {
2728
t.Errorf("expect %v code, got %v", e, a)
2829
}
2930
}
31+
32+
func TestBuildableHTTPClient_WithTimeout(t *testing.T) {
33+
client := &aws.BuildableHTTPClient{}
34+
35+
expect := 10 * time.Millisecond
36+
client2 := client.WithTimeout(expect).(*aws.BuildableHTTPClient)
37+
38+
if e, a := time.Duration(0), client.GetTimeout(); e != a {
39+
t.Errorf("expect %v initial timeout, got %v", e, a)
40+
}
41+
42+
if e, a := expect, client2.GetTimeout(); e != a {
43+
t.Errorf("expect %v timeout, got %v", e, a)
44+
}
45+
}

aws/request.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,15 @@ type Request struct {
118118
SignedHeaderVals http.Header
119119
LastSignedAt time.Time
120120

121+
// The time the response headers were received from the API call.
122+
ResponseAt time.Time
123+
124+
// AttemptClockSkews are the estimated clock skew between the service
125+
// response Date header, and when the SDK received the response per
126+
// attempt. This estimate will include the latency of receiving the
127+
// service's response headers.
128+
AttemptClockSkews []time.Duration
129+
121130
// ID for this operation's request that is shared across attempts.
122131
InvocationID string
123132

0 commit comments

Comments
 (0)