-
Notifications
You must be signed in to change notification settings - Fork 31
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: Add retry information headers #692
Conversation
…imeout & estimatedSkew).
…o middleware context.
…tring & one that calculates TTL by adding estimated skew and socket timeout to current time according to local machine clock.
…ed HTTP response's Date header value.
…rt to string without any adjustments.
…t existing RetryIntegrationTests to check retry information headers in inputs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comments to help with PR reviews.
@@ -16,23 +22,36 @@ public struct RetryMiddleware<Strategy: RetryStrategy, | |||
public var id: String { "Retry" } | |||
public var strategy: Strategy | |||
|
|||
// The UUID string used to uniquely identify an API call and all of its subsequent retries. | |||
private let invocationID = UUID().uuidString.lowercased() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This invocation ID the unique string that identifies a single SDK operation invocation. In other words, the initial request & possible subsequent retried requests sent on behalf of it all share this unique ID.
@@ -5,6 +5,12 @@ | |||
// SPDX-License-Identifier: Apache-2.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make RetryMiddleware add amz-sdk-invocation-id
header and amz-sdk-request
header to the request.
amz-sdk-invocation-id
: just a UUID value generated for the given operation invocation. Shared between origianl request and all subsequent retried requests.amz-sdk-request
: has three values.ttl=
,attempt=
, andmax=
. For the first, original rquest, TTL is not included. Starting from first retry request, TTL is calculated by adding socket timeout & estimated skew values to current time. BothsocketTimeout
andestimatedSkew
are taken fromcontext
.socketTimeout
is saved tocontext
atcontext
creation upstream, in operation method body.estimatedSkew
is calculated then saved tocontext
byDeserializeMiddleware
when HTTP response is first received and deserialized.
// The UUID string used to uniquely identify an API call and all of its subsequent retries. | ||
private let invocationID = UUID().uuidString.lowercased() | ||
// Max number of retries configured for retry strategy. | ||
private var maxRetries: Int |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the value used for max=
value in amz-sdk-request
header.
let partitionID = try getPartitionID(context: context, input: input) | ||
let token = try await strategy.acquireInitialRetryToken(tokenScope: partitionID) | ||
return try await sendRequest(token: token, context: context, input: input, next: next) | ||
input.headers.add(name: "amz-sdk-request", value: "attempt=1; max=\(maxRetries)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add amz-sdk-request
header with TTL excluded for the first, original request.
input.headers.update( | ||
name: "amz-sdk-request", | ||
value: "ttl=\(ttlDateUTCString); attempt=\(attemptNumber + 1); max=\(maxRetries)" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update the amz-sdk-request
header. Update attempt number & TTL.
// Calculates & returns TTL datetime in strftime format `YYYYmmddTHHMMSSZ`. | ||
func getTTL(now: Date, estimatedSkew: TimeInterval, socketTimeout: TimeInterval) -> String { | ||
let dateFormatter = DateFormatter() | ||
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" | ||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") | ||
dateFormatter.timeZone = TimeZone(abbreviation: "GMT") | ||
let ttlDate = now.addingTimeInterval(estimatedSkew + socketTimeout) | ||
return dateFormatter.string(from: ttlDate) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Utility method for adding socketTimeout
and estimatedSkew
to given Date
. Returns the sum as a string in strftime format.
// Calculates & returns estimated skew. | ||
func getEstimatedSkew(now: Date, responseDateString: String) -> TimeInterval { | ||
let dateFormatter = DateFormatter() | ||
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z" | ||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") | ||
dateFormatter.timeZone = TimeZone(abbreviation: "GMT") | ||
let responseDate: Date = dateFormatter.date(from: responseDateString) ?? now | ||
// (Estimated skew) = (Date header from HTTP response) - (client's current time)). | ||
return responseDate.timeIntervalSince(now) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Utility method for subtracting given Date
from responseDateString
. Returns the difference in seconds (TimeInterval
). This method is used in DeserializeMiddleware
but placed here to group TTL related things in one place.
if let responseDateString = response.httpResponse.headers.value(for: "Date") { | ||
let estimatedSkew = getEstimatedSkew(now: Date(), responseDateString: responseDateString) | ||
context.attributes.set(key: AttributeKeys.estimatedSkew, value: estimatedSkew) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calculate estimatedSkew
by getting difference between server's timestamp on HTTP response returned via the Date
header and current client time. Save to context
for use in RetryMiddleware
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when the response body takes a long time to transmit due to size or bad network? Does that time affect this number?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bad network would increase estimated skew because network latency is included in the estimated skew.
As for response body taking a long time to transmit, that would increase estimated skew as well since that would delay when the response is deserialized which would subsequently delay when the estimated skew gets calculated.
@@ -45,7 +64,24 @@ public struct RetryMiddleware<Strategy: RetryStrategy, | |||
// TODO: log token error here | |||
throw operationError | |||
} | |||
return try await sendRequest(token: token, context: context, input: input, next: next) | |||
guard let estimatedSkew: TimeInterval = context.attributes.get(key: AttributeKeys.estimatedSkew) else { | |||
throw ClientError.dataNotFound("Estimated skew not found; failed to calculate TTL for retry.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to fail the entire request for lack of a parameter for this header?
Not sure what could cause this, but I don't think we want to fail customer requests for lack of this header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to logging .info level message with logger & using default values.
let dateFormatter = DateFormatter() | ||
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" | ||
dateFormatter.locale = Locale(identifier: "en_US_POSIX") | ||
dateFormatter.timeZone = TimeZone(abbreviation: "GMT") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: use TZ identifier "UTC" for server communications
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to UTC.
@@ -45,7 +45,7 @@ public class HttpClientConfiguration { | |||
/// - protocolType: The HTTP scheme (`http` or `https`) to be used for API requests. Defaults to the operation's standard configuration. | |||
public init( | |||
connectTimeout: TimeInterval? = nil, | |||
socketTimeout: TimeInterval? = nil, | |||
socketTimeout: TimeInterval? = 60.0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are providing a non-nil default, should the param be non-optional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed as suggested.
if let responseDateString = response.httpResponse.headers.value(for: "Date") { | ||
let estimatedSkew = getEstimatedSkew(now: Date(), responseDateString: responseDateString) | ||
context.attributes.set(key: AttributeKeys.estimatedSkew, value: estimatedSkew) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when the response body takes a long time to transmit due to size or bad network? Does that time affect this number?
let responseDateStringPlusTen = "Mon, 15 Jul 2024 01:24:22 GMT" | ||
let estimatedSkew = getEstimatedSkew(now: responseDate, responseDateString: responseDateStringPlusTen) | ||
|
||
XCTAssert(estimatedSkew == 10.0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Use XCTAssertEqual
to compare two values in a test assertion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed as suggested.
…hrowing an error.
SmithyRetriesTests test target was accidentally removed from Package.swift in smithy-lang#692, so this commit adds it back. The test was also not working properly, because Context.partitionID's attribute key name used by RetryMiddleware was different from the one being set in the test. This caused the test to exit early when partitionID wasn't found.
Companion PR: awslabs/aws-sdk-swift#1432
Issue #
awslabs/aws-sdk-swift#1108
Description of changes
--/seps/accepted/shared/retry-header.md
Misc:
Scope
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.