Skip to content

Commit d887259

Browse files
authored
feat: add codegen wrappers for retries (#490)
1 parent 74af3aa commit d887259

File tree

27 files changed

+576
-132
lines changed

27 files changed

+576
-132
lines changed

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jacocoVersion=0.8.7
3636
kotlinxBenchmarkVersion=0.3.1
3737

3838
# serialization
39+
kamlVersion=0.36.0
3940
xmlpullVersion=1.1.3.1
4041
xpp3Version=1.1.6
4142

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/Headers.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
*/
55
package aws.smithy.kotlin.runtime.http
66

7-
import aws.smithy.kotlin.runtime.http.util.StringValuesMap
8-
import aws.smithy.kotlin.runtime.http.util.StringValuesMapBuilder
7+
import aws.smithy.kotlin.runtime.http.util.*
98
import aws.smithy.kotlin.runtime.http.util.StringValuesMapImpl
109

1110
/**
@@ -35,12 +34,13 @@ private object EmptyHeaders : Headers {
3534
/**
3635
* Build an immutable HTTP header map
3736
*/
38-
class HeadersBuilder : StringValuesMapBuilder(true, 8) {
37+
class HeadersBuilder : StringValuesMapBuilder(true, 8), CanDeepCopy<HeadersBuilder> {
3938
override fun toString(): String = "HeadersBuilder ${entries()} "
40-
override fun build(): Headers {
41-
require(!built) { "HeadersBuilder can only build a single Headers instance" }
42-
built = true
43-
return HeadersImpl(values)
39+
override fun build(): Headers = HeadersImpl(values)
40+
41+
override fun deepCopy(): HeadersBuilder {
42+
val originalValues = values.deepCopy()
43+
return HeadersBuilder().apply { values.putAll(originalValues) }
4444
}
4545
}
4646

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import aws.smithy.kotlin.runtime.io.SdkByteReadChannel
1111
* HTTP payload to be sent to a peer
1212
*/
1313
sealed class HttpBody {
14-
1514
/**
1615
* Specifies the length of this [HttpBody] content
1716
* If null it is assumed to be a streaming source using e.g. `Transfer-Encoding: Chunked`

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/QueryParameters.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
*/
55
package aws.smithy.kotlin.runtime.http
66

7-
import aws.smithy.kotlin.runtime.http.util.StringValuesMap
8-
import aws.smithy.kotlin.runtime.http.util.StringValuesMapBuilder
9-
import aws.smithy.kotlin.runtime.http.util.StringValuesMapImpl
7+
import aws.smithy.kotlin.runtime.http.util.*
108
import aws.smithy.kotlin.runtime.util.text.urlEncodeComponent
119

1210
/**
@@ -33,12 +31,13 @@ private object EmptyQueryParameters : QueryParameters {
3331
override fun isEmpty(): Boolean = true
3432
}
3533

36-
class QueryParametersBuilder : StringValuesMapBuilder(true, 8) {
34+
class QueryParametersBuilder : StringValuesMapBuilder(true, 8), CanDeepCopy<QueryParametersBuilder> {
3735
override fun toString(): String = "QueryParametersBuilder ${entries()} "
38-
override fun build(): QueryParameters {
39-
require(!built) { "QueryParametersBuilder can only build a single instance" }
40-
built = true
41-
return QueryParametersImpl(values)
36+
override fun build(): QueryParameters = QueryParametersImpl(values)
37+
38+
override fun deepCopy(): QueryParametersBuilder {
39+
val originalValues = values.deepCopy()
40+
return QueryParametersBuilder().apply { values.putAll(originalValues) }
4241
}
4342
}
4443

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/Url.kt

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55
package aws.smithy.kotlin.runtime.http
66

7+
import aws.smithy.kotlin.runtime.http.util.CanDeepCopy
78
import aws.smithy.kotlin.runtime.util.InternalApi
89
import aws.smithy.kotlin.runtime.util.text.encodeUrlPath
910

@@ -105,7 +106,7 @@ data class UserInfo(val username: String, val password: String)
105106
/**
106107
* Construct a URL by it's individual components
107108
*/
108-
class UrlBuilder {
109+
class UrlBuilder : CanDeepCopy<UrlBuilder> {
109110
var scheme = Protocol.HTTPS
110111
var host: String = ""
111112
var port: Int? = null
@@ -130,6 +131,20 @@ class UrlBuilder {
130131
forceQuery
131132
)
132133

134+
override fun deepCopy(): UrlBuilder {
135+
val builder = this
136+
return UrlBuilder().apply {
137+
scheme = builder.scheme
138+
host = builder.host
139+
port = builder.port
140+
path = builder.path
141+
parameters = builder.parameters.deepCopy()
142+
fragment = builder.fragment
143+
userInfo = builder.userInfo?.copy()
144+
forceQuery = builder.forceQuery
145+
}
146+
}
147+
133148
override fun toString(): String =
134149
"UrlBuilder(scheme=$scheme, host='$host', port=$port, path='$path', parameters=$parameters, fragment=$fragment, userInfo=$userInfo, forceQuery=$forceQuery)"
135150
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.http.middleware
7+
8+
import aws.smithy.kotlin.runtime.http.Feature
9+
import aws.smithy.kotlin.runtime.http.FeatureKey
10+
import aws.smithy.kotlin.runtime.http.HttpBody
11+
import aws.smithy.kotlin.runtime.http.HttpClientFeatureFactory
12+
import aws.smithy.kotlin.runtime.http.operation.SdkHttpOperation
13+
import aws.smithy.kotlin.runtime.http.operation.deepCopy
14+
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
15+
import aws.smithy.kotlin.runtime.retries.RetryPolicy
16+
import aws.smithy.kotlin.runtime.retries.RetryStrategy
17+
18+
class RetryFeature(private val strategy: RetryStrategy, private val policy: RetryPolicy<Any?>) : Feature {
19+
class Config {
20+
var strategy: RetryStrategy? = null
21+
var policy: RetryPolicy<Any?>? = null
22+
}
23+
24+
companion object Feature : HttpClientFeatureFactory<Config, RetryFeature> {
25+
override val key: FeatureKey<RetryFeature> = FeatureKey("RetryFeature")
26+
27+
override fun create(block: Config.() -> Unit): RetryFeature {
28+
val config = Config().apply(block)
29+
val strategy = requireNotNull(config.strategy) { "strategy is required" }
30+
val policy = requireNotNull(config.policy) { "policy is required" }
31+
return RetryFeature(strategy, policy)
32+
}
33+
}
34+
35+
override fun <I, O> install(operation: SdkHttpOperation<I, O>) {
36+
operation.execution.finalize.intercept { req, next ->
37+
if (req.subject.isRetryable) {
38+
strategy.retry(policy) {
39+
// Deep copy the request because later middlewares (e.g., signing) mutate it
40+
val reqCopy = req.deepCopy()
41+
42+
when (val body = reqCopy.subject.body) {
43+
// Reset streaming bodies back to beginning
44+
is HttpBody.Streaming -> body.reset()
45+
}
46+
47+
next.call(reqCopy)
48+
}
49+
} else {
50+
next.call(req)
51+
}
52+
}
53+
}
54+
}
55+
56+
/**
57+
* Indicates whether this HTTP request could be retried. Some requests with streaming bodies are unsuitable for
58+
* retries.
59+
*/
60+
val HttpRequestBuilder.isRetryable: Boolean
61+
get() = when (val body = this.body) {
62+
is HttpBody.Empty, is HttpBody.Bytes -> true
63+
is HttpBody.Streaming -> body.isReplayable
64+
}

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/operation/OperationRequest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package aws.smithy.kotlin.runtime.http.operation
77

88
import aws.smithy.kotlin.runtime.client.ExecutionContext
9+
import aws.smithy.kotlin.runtime.http.util.CanDeepCopy
910

1011
/**
1112
* Wrapper around a type [subject] with an execution context.
@@ -17,3 +18,11 @@ import aws.smithy.kotlin.runtime.client.ExecutionContext
1718
* @param subject The input type
1819
*/
1920
data class OperationRequest<T>(val context: ExecutionContext, val subject: T)
21+
22+
/**
23+
* Deep copy an [OperationRequest] to a new request. Note that, because [context] is...well, context...it's considered
24+
* transient and is not part of the copy. The subject itself, however, is deeply copied.
25+
* @return A new [OperationRequest] with the same context and a deeply-copied subject.
26+
*/
27+
internal fun <T : CanDeepCopy<T>> OperationRequest<T>.deepCopy(): OperationRequest<T> =
28+
OperationRequest(context, subject.deepCopy())

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/request/HttpRequestBuilder.kt

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,30 @@ package aws.smithy.kotlin.runtime.http.request
66

77
import aws.smithy.kotlin.runtime.http.*
88
import aws.smithy.kotlin.runtime.http.content.ByteArrayContent
9+
import aws.smithy.kotlin.runtime.http.util.CanDeepCopy
910
import aws.smithy.kotlin.runtime.io.*
1011
import aws.smithy.kotlin.runtime.util.InternalApi
1112

1213
/**
1314
* Used to construct an HTTP request
15+
* @param method The HTTP method (verb) to use when making the request
16+
* @param url Endpoint to make request to
17+
* @param headers HTTP headers
18+
* @param body Outgoing payload. Initially empty
1419
*/
15-
class HttpRequestBuilder {
16-
/**
17-
* The HTTP method (verb) to use when making the request
18-
*/
19-
var method: HttpMethod = HttpMethod.GET
20-
21-
/**
22-
* Endpoint to make request to
23-
*/
24-
val url: UrlBuilder = UrlBuilder()
25-
26-
/**
27-
* HTTP headers
28-
*/
29-
val headers: HeadersBuilder = HeadersBuilder()
30-
31-
/**
32-
* Outgoing payload. Initially empty
33-
*/
34-
var body: HttpBody = HttpBody.Empty
20+
class HttpRequestBuilder private constructor(
21+
var method: HttpMethod,
22+
val url: UrlBuilder,
23+
val headers: HeadersBuilder,
24+
var body: HttpBody,
25+
) : CanDeepCopy<HttpRequestBuilder> {
26+
constructor() : this(HttpMethod.GET, UrlBuilder(), HeadersBuilder(), HttpBody.Empty)
3527

3628
fun build(): HttpRequest = HttpRequest(method, url.build(), if (headers.isEmpty()) Headers.Empty else headers.build(), body)
3729

30+
override fun deepCopy(): HttpRequestBuilder =
31+
HttpRequestBuilder(method, url.deepCopy(), headers.deepCopy(), body)
32+
3833
override fun toString(): String = buildString {
3934
append("HttpRequestBuilder(method=$method, url=$url, headers=$headers, body=$body)")
4035
}

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/retries/StandardRetryFeature.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.http.util
7+
8+
/**
9+
* Indicates that an object supports a [deepCopy] operation which will return a copy that can be safely mutated without
10+
* affecting other instances.
11+
*/
12+
interface CanDeepCopy<out T> {
13+
/**
14+
* Returns a deep copy of this object.
15+
*/
16+
fun deepCopy(): T
17+
}

0 commit comments

Comments
 (0)