-
Notifications
You must be signed in to change notification settings - Fork 29
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 codegen wrappers for retries #490
Changes from 1 commit
dfd0a6b
cb8cfad
e07a541
86975a2
fcf4bfe
3f16d51
5cf9224
6c3f563
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,12 +5,17 @@ | |
package aws.smithy.kotlin.runtime.http | ||
|
||
import aws.smithy.kotlin.runtime.content.ByteStream | ||
import aws.smithy.kotlin.runtime.http.util.CanDeepCopy | ||
import aws.smithy.kotlin.runtime.io.SdkByteReadChannel | ||
|
||
/** | ||
* HTTP payload to be sent to a peer | ||
*/ | ||
sealed class HttpBody { | ||
sealed class HttpBody : CanDeepCopy<HttpBody> { | ||
/** | ||
* Flag indicating the body can be consumed multiple times. | ||
*/ | ||
open val isReplayable: Boolean = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question/style why move this? The only type we expect to need to "replay" is streaming right? |
||
|
||
/** | ||
* Specifies the length of this [HttpBody] content | ||
|
@@ -23,6 +28,7 @@ sealed class HttpBody { | |
*/ | ||
object Empty : HttpBody() { | ||
override val contentLength: Long = 0 | ||
override fun deepCopy(): HttpBody = this // Deep copies are unnecessary for empty bodies | ||
} | ||
|
||
/** | ||
|
@@ -35,6 +41,12 @@ sealed class HttpBody { | |
* Provides [ByteArray] to be sent to peer | ||
*/ | ||
abstract fun bytes(): ByteArray | ||
|
||
override fun deepCopy(): Bytes = object : Bytes() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. comment I don't love that we are going to deep copy a payload. I'm not convinced this type should implement |
||
private val copiedBytes = this@Bytes.bytes().copyOf() | ||
override fun bytes(): ByteArray = copiedBytes | ||
override val contentLength: Long? = copiedBytes.size.toLong() | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -52,14 +64,20 @@ sealed class HttpBody { | |
* Flag indicating the stream can be consumed multiple times. If `false` [reset] will throw an | ||
* [UnsupportedOperationException]. | ||
*/ | ||
open val isReplayable: Boolean = false | ||
override val isReplayable: Boolean = false | ||
|
||
/** | ||
* Reset the stream such that the next call to [readFrom] provides a fresh channel. | ||
* @throws UnsupportedOperationException if the stream can only be consumed once. Consumers can check | ||
* [isReplayable] before calling | ||
*/ | ||
open fun reset() { throw UnsupportedOperationException("${this::class.simpleName} can only be consumed once") } | ||
open fun reset() { throwSingleConsumptionException() } | ||
|
||
/** | ||
* Throw a general exception upon attempting to consume the stream more than once. | ||
*/ | ||
protected fun throwSingleConsumptionException(): Nothing = | ||
throw UnsupportedOperationException("${this::class.simpleName} can only be consumed once") | ||
} | ||
} | ||
|
||
|
@@ -73,11 +91,13 @@ fun ByteStream.toHttpBody(): HttpBody = when (val byteStream = this) { | |
} | ||
is ByteStream.OneShotStream -> object : HttpBody.Streaming() { | ||
override val contentLength: Long? = byteStream.contentLength | ||
override fun deepCopy(): Streaming = throwSingleConsumptionException() | ||
override fun readFrom(): SdkByteReadChannel = byteStream.readFrom() | ||
} | ||
is ByteStream.ReplayableStream -> object : HttpBody.Streaming() { | ||
private var channel: SdkByteReadChannel? = null | ||
override val contentLength: Long? = byteStream.contentLength | ||
override fun deepCopy(): Streaming = this // Replayable streams copy themselves by default | ||
override fun readFrom(): SdkByteReadChannel = channel ?: byteStream.newReader().also { channel = it } | ||
override val isReplayable: Boolean = true | ||
override fun reset() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,9 +4,7 @@ | |
*/ | ||
package aws.smithy.kotlin.runtime.http | ||
|
||
import aws.smithy.kotlin.runtime.http.util.StringValuesMap | ||
import aws.smithy.kotlin.runtime.http.util.StringValuesMapBuilder | ||
import aws.smithy.kotlin.runtime.http.util.StringValuesMapImpl | ||
import aws.smithy.kotlin.runtime.http.util.* | ||
import aws.smithy.kotlin.runtime.util.text.urlEncodeComponent | ||
|
||
/** | ||
|
@@ -33,13 +31,12 @@ private object EmptyQueryParameters : QueryParameters { | |
override fun isEmpty(): Boolean = true | ||
} | ||
|
||
class QueryParametersBuilder : StringValuesMapBuilder(true, 8) { | ||
class QueryParametersBuilder : StringValuesMapBuilder(true, 8), CanDeepCopy<QueryParametersBuilder> { | ||
override fun toString(): String = "QueryParametersBuilder ${entries()} " | ||
override fun build(): QueryParameters { | ||
require(!built) { "QueryParametersBuilder can only build a single instance" } | ||
built = true | ||
return QueryParametersImpl(values) | ||
} | ||
override fun build(): QueryParameters = QueryParametersImpl(values) | ||
|
||
override fun deepCopy(): QueryParametersBuilder = | ||
QueryParametersBuilder().apply { values.putAll(this@QueryParametersBuilder.values.deepCopy()) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style I found this hard to read/follow due to |
||
} | ||
|
||
fun Map<String, String>.toQueryParameters(): QueryParameters { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
*/ | ||
package aws.smithy.kotlin.runtime.http | ||
|
||
import aws.smithy.kotlin.runtime.http.util.CanDeepCopy | ||
import aws.smithy.kotlin.runtime.util.InternalApi | ||
import aws.smithy.kotlin.runtime.util.text.encodeUrlPath | ||
|
||
|
@@ -105,7 +106,7 @@ data class UserInfo(val username: String, val password: String) | |
/** | ||
* Construct a URL by it's individual components | ||
*/ | ||
class UrlBuilder { | ||
class UrlBuilder : CanDeepCopy<UrlBuilder> { | ||
var scheme = Protocol.HTTPS | ||
var host: String = "" | ||
var port: Int? = null | ||
|
@@ -130,6 +131,17 @@ class UrlBuilder { | |
forceQuery | ||
) | ||
|
||
override fun deepCopy(): UrlBuilder = UrlBuilder().apply { | ||
scheme = this@UrlBuilder.scheme | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style another ambiguous override fun deepCopy(): UrlBuilder {
val builder = this
return UrlBuilder().apply {
scheme = builder.scheme
...
}
} |
||
host = this@UrlBuilder.host | ||
port = this@UrlBuilder.port | ||
path = this@UrlBuilder.path | ||
parameters = this@UrlBuilder.parameters.deepCopy() | ||
fragment = this@UrlBuilder.fragment | ||
userInfo = this@UrlBuilder.userInfo?.copy() | ||
forceQuery = this@UrlBuilder.forceQuery | ||
} | ||
|
||
override fun toString(): String = | ||
"UrlBuilder(scheme=$scheme, host='$host', port=$port, path='$path', parameters=$parameters, fragment=$fragment, userInfo=$userInfo, forceQuery=$forceQuery)" | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
package aws.smithy.kotlin.runtime.http.operation | ||
|
||
import aws.smithy.kotlin.runtime.client.ExecutionContext | ||
import aws.smithy.kotlin.runtime.http.util.CanDeepCopy | ||
|
||
/** | ||
* Wrapper around a type [subject] with an execution context. | ||
|
@@ -17,3 +18,11 @@ import aws.smithy.kotlin.runtime.client.ExecutionContext | |
* @param subject The input type | ||
*/ | ||
data class OperationRequest<T>(val context: ExecutionContext, val subject: T) | ||
|
||
/** | ||
* Deep copy an [OperationRequest] to a new request. Note that, because [context] is...well, context...it's considered | ||
* transient and is not part of the copy. The subject itself, however, is deeply copied. | ||
* @return A new [OperationRequest] with the same context and a deeply-copied subject. | ||
*/ | ||
fun <T : CanDeepCopy<T>> OperationRequest<T>.deepCopy(): OperationRequest<T> = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion Make |
||
OperationRequest(context, subject.deepCopy()) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,35 +6,37 @@ package aws.smithy.kotlin.runtime.http.request | |
|
||
import aws.smithy.kotlin.runtime.http.* | ||
import aws.smithy.kotlin.runtime.http.content.ByteArrayContent | ||
import aws.smithy.kotlin.runtime.http.util.CanDeepCopy | ||
import aws.smithy.kotlin.runtime.io.* | ||
import aws.smithy.kotlin.runtime.util.InternalApi | ||
|
||
/** | ||
* Used to construct an HTTP request | ||
* @param method The HTTP method (verb) to use when making the request | ||
* @param url Endpoint to make request to | ||
* @param headers HTTP headers | ||
* @param body Outgoing payload. Initially empty | ||
*/ | ||
class HttpRequestBuilder { | ||
class HttpRequestBuilder private constructor( | ||
var method: HttpMethod, | ||
val url: UrlBuilder, | ||
val headers: HeadersBuilder, | ||
var body: HttpBody, | ||
) : CanDeepCopy<HttpRequestBuilder> { | ||
/** | ||
* The HTTP method (verb) to use when making the request | ||
* Indicates whether this HTTP request could be retried. Some requests with streaming bodies are unsuitable for | ||
* retries. | ||
*/ | ||
var method: HttpMethod = HttpMethod.GET | ||
val isRetryable: Boolean | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit seems like this can be localized to the retry middleware as an extension |
||
get() = body.isReplayable | ||
|
||
/** | ||
* Endpoint to make request to | ||
*/ | ||
val url: UrlBuilder = UrlBuilder() | ||
|
||
/** | ||
* HTTP headers | ||
*/ | ||
val headers: HeadersBuilder = HeadersBuilder() | ||
|
||
/** | ||
* Outgoing payload. Initially empty | ||
*/ | ||
var body: HttpBody = HttpBody.Empty | ||
constructor() : this(HttpMethod.GET, UrlBuilder(), HeadersBuilder(), HttpBody.Empty) | ||
|
||
fun build(): HttpRequest = HttpRequest(method, url.build(), if (headers.isEmpty()) Headers.Empty else headers.build(), body) | ||
|
||
override fun deepCopy(): HttpRequestBuilder = | ||
HttpRequestBuilder(method, url.deepCopy(), headers.deepCopy(), body.deepCopy()) | ||
|
||
override fun toString(): String = buildString { | ||
append("HttpRequestBuilder(method=$method, url=$url, headers=$headers, body=$body)") | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0. | ||
*/ | ||
|
||
package aws.smithy.kotlin.runtime.http.util | ||
|
||
interface CanDeepCopy<T> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style
|
||
fun deepCopy(): T | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,8 +64,10 @@ internal open class StringValuesMapImpl( | |
override val caseInsensitiveName: Boolean = false, | ||
initialValues: Map<String, List<String>> = emptyMap() | ||
) : StringValuesMap { | ||
protected val values: Map<String, List<String>> by lazy { | ||
if (caseInsensitiveName) CaseInsensitiveMap<List<String>>().apply { putAll(initialValues) } else values.toMap() | ||
protected val values: Map<String, List<String>> = run { | ||
// Make a defensive copy so modifications to the initialValues don't mutate our internal copy | ||
val copiedValues = initialValues.deepCopy() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question is this really necessary? The input There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm afraid it is necessary. The type of the parameter may be the immutable interface type Moreover, the map values passed in are mutable lists. They too need to be defensively copied to prevent post hoc mutation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which argument where? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (caseInsensitiveName) CaseInsensitiveMap<List<String>>().apply { putAll(copiedValues) } else copiedValues | ||
} | ||
|
||
override fun getAll(name: String): List<String>? = values[name] | ||
|
@@ -81,13 +83,17 @@ internal open class StringValuesMapImpl( | |
override fun isEmpty(): Boolean = values.isEmpty() | ||
} | ||
|
||
/** | ||
* Perform a deep copy of this map, specifically duplicating the value lists so that they're insulated from changes. | ||
* @return A new map instance with copied value lists. | ||
*/ | ||
internal fun Map<String, List<String>>.deepCopy() = mapValues { (_, v) -> v.toMutableList() } | ||
|
||
@InternalApi | ||
open class StringValuesMapBuilder(val caseInsensitiveName: Boolean = false, size: Int = 8) { | ||
protected val values: MutableMap<String, MutableList<String>> = | ||
if (caseInsensitiveName) CaseInsensitiveMap() else LinkedHashMap(size) | ||
|
||
protected var built = false | ||
|
||
fun getAll(name: String): List<String>? = values[name] | ||
|
||
operator fun contains(name: String): Boolean = name in values | ||
|
@@ -154,16 +160,8 @@ open class StringValuesMapBuilder(val caseInsensitiveName: Boolean = false, size | |
|
||
fun clear() = values.clear() | ||
|
||
open fun build(): StringValuesMap { | ||
require(!built) { "StringValuesMapBuilder can only build a single instance" } | ||
built = true | ||
return StringValuesMapImpl(caseInsensitiveName, values) | ||
} | ||
open fun build(): StringValuesMap = StringValuesMapImpl(caseInsensitiveName, values) | ||
|
||
private fun ensureListForKey(name: String, size: Int): MutableList<String> { | ||
if (built) { | ||
throw IllegalStateException("Cannot modify a builder when final structure has already been built") | ||
} | ||
return values[name] ?: ArrayList<String>(size).also { values[name] = it } | ||
} | ||
private fun ensureListForKey(name: String, size: Int): MutableList<String> = | ||
values[name] ?: ArrayList<String>(size).also { values[name] = it } | ||
} |
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.
question
Does this really need to support deep copy? Payload bytes aren't modified by signing (and they really shouldn't be modified by any middleware)