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: Add retry information headers #692

Merged
merged 18 commits into from Apr 16, 2024
Merged

Conversation

sichanyoo
Copy link
Contributor

@sichanyoo sichanyoo commented Apr 5, 2024

Companion PR: awslabs/aws-sdk-swift#1432

Issue #

awslabs/aws-sdk-swift#1108

Description of changes

  • Adds retry information headers according to SEP: --/seps/accepted/shared/retry-header.md
  • Adds tests for verifying header contents in inputs.

Misc:

  • Organize test files and directories to mirror source structure

Scope

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Copy link
Contributor Author

@sichanyoo sichanyoo left a 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()
Copy link
Contributor Author

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
Copy link
Contributor Author

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=, and max=. 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. Both socketTimeout and estimatedSkew are taken from context. socketTimeout is saved to context at context creation upstream, in operation method body. estimatedSkew is calculated then saved to context by DeserializeMiddleware 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
Copy link
Contributor Author

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)")
Copy link
Contributor Author

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.

Comment on lines +74 to +77
input.headers.update(
name: "amz-sdk-request",
value: "ttl=\(ttlDateUTCString); attempt=\(attemptNumber + 1); max=\(maxRetries)"
)
Copy link
Contributor Author

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.

Comment on lines 106 to 114
// 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)
}
Copy link
Contributor Author

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.

Comment on lines +116 to +125
// 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)
}
Copy link
Contributor Author

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.

Comment on lines +30 to +33
if let responseDateString = response.httpResponse.headers.value(for: "Date") {
let estimatedSkew = getEstimatedSkew(now: Date(), responseDateString: responseDateString)
context.attributes.set(key: AttributeKeys.estimatedSkew, value: estimatedSkew)
}
Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.")
Copy link
Contributor

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.

Copy link
Contributor Author

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")
Copy link
Contributor

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

Copy link
Contributor Author

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,
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed as suggested.

Comment on lines +30 to +33
if let responseDateString = response.httpResponse.headers.value(for: "Date") {
let estimatedSkew = getEstimatedSkew(now: Date(), responseDateString: responseDateString)
context.attributes.set(key: AttributeKeys.estimatedSkew, value: estimatedSkew)
}
Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed as suggested.

@sichanyoo sichanyoo requested a review from jbelkins April 11, 2024 18:38
@sichanyoo sichanyoo merged commit 7be2bd8 into main Apr 16, 2024
12 checks passed
@sichanyoo sichanyoo deleted the feat/retry-information-headers branch April 16, 2024 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants