Skip to content

Commit 4d89b3b

Browse files
authored
feat(rt): http engine config (#336)
1 parent 3b18b87 commit 4d89b3b

File tree

6 files changed

+128
-23
lines changed

6 files changed

+128
-23
lines changed

.github/ISSUE_TEMPLATE/feature_request.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ labels: enhancement, needs-triage
3030
<!--- Any alternative solutions or features you've considered -->
3131

3232
## Additional Context
33-
<!--- How has the lack of this feaure affected you? What are you trying to accomplish? -->
33+
<!--- How has the lack of this feature affected you? What are you trying to accomplish? -->
3434
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
3535

3636

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/config/imds/ImdsClient.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import aws.sdk.kotlin.runtime.http.middleware.UserAgent
1717
import aws.smithy.kotlin.runtime.client.ExecutionContext
1818
import aws.smithy.kotlin.runtime.http.*
1919
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
20-
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
2120
import aws.smithy.kotlin.runtime.http.operation.*
2221
import aws.smithy.kotlin.runtime.http.response.HttpResponse
2322
import aws.smithy.kotlin.runtime.io.Closeable
@@ -57,8 +56,16 @@ public class ImdsClient private constructor(builder: Builder) : Closeable {
5756
private val tokenTtl: Duration = builder.tokenTTL
5857
private val clock: Clock = builder.clock
5958
private val platformProvider: PlatformProvider = builder.platformProvider
59+
private val httpClient: SdkHttpClient
6060

6161
init {
62+
val engine = builder.engine ?: CrtHttpEngine {
63+
connectTimeout = Duration.seconds(1)
64+
socketReadTimeout = Duration.seconds(1)
65+
}
66+
67+
httpClient = sdkHttpClient(engine)
68+
6269
// validate the override at construction time
6370
if (endpointConfiguration is EndpointConfiguration.Custom) {
6471
val url = endpointConfiguration.endpoint.toUrl()
@@ -70,9 +77,6 @@ public class ImdsClient private constructor(builder: Builder) : Closeable {
7077
}
7178
}
7279

73-
// TODO connect/socket timeouts
74-
private val httpClient = sdkHttpClient(builder.engine ?: CrtHttpEngine(HttpClientEngineConfig()))
75-
7680
// cached middleware instances
7781
private val middleware: List<Feature> = listOf(
7882
ServiceEndpointResolver.create {

aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/config/imds/ImdsClientTest.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ import aws.smithy.kotlin.runtime.http.request.url
1616
import aws.smithy.kotlin.runtime.http.response.HttpResponse
1717
import aws.smithy.kotlin.runtime.httptest.TestConnection
1818
import aws.smithy.kotlin.runtime.httptest.buildTestConnection
19+
import aws.smithy.kotlin.runtime.time.Instant
1920
import aws.smithy.kotlin.runtime.time.ManualClock
21+
import aws.smithy.kotlin.runtime.time.epochMilliseconds
2022
import io.kotest.matchers.string.shouldContain
23+
import kotlinx.coroutines.withTimeout
2124
import kotlinx.serialization.json.*
2225
import kotlin.test.*
2326
import kotlin.time.Duration
@@ -203,11 +206,23 @@ class ImdsClientTest {
203206
fail("not implemented yet")
204207
}
205208

206-
@Ignore
207209
@Test
208-
fun testHttpConnectTimeouts() {
209-
// Need a 1 sec connect timeout + other timeouts in imds spec
210-
fail("not implemented yet")
210+
fun testHttpConnectTimeouts(): Unit = runSuspendTest {
211+
// end-to-end real client times out after 1-second
212+
val client = ImdsClient {
213+
// will never resolve
214+
endpointConfiguration = EndpointConfiguration.Custom("http://240.0.0.0".toEndpoint())
215+
}
216+
217+
val start = Instant.now()
218+
assertFails {
219+
withTimeout(3000) {
220+
client.get("/latest/metadata")
221+
}
222+
}.message.shouldContain("timed out")
223+
val elapsed = Instant.now().epochMilliseconds - start.epochMilliseconds
224+
assertTrue(elapsed >= 1000)
225+
assertTrue(elapsed < 2000)
211226
}
212227

213228
data class ImdsConfigTest(

aws-runtime/http-client-engine-crt/common/src/aws/sdk/kotlin/runtime/http/engine/crt/CrtHttpEngine.kt

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,74 @@ package aws.sdk.kotlin.runtime.http.engine.crt
77

88
import aws.sdk.kotlin.crt.http.*
99
import aws.sdk.kotlin.crt.io.*
10+
import aws.sdk.kotlin.runtime.ClientException
1011
import aws.sdk.kotlin.runtime.crt.SdkDefaultIO
1112
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
1213
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
13-
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
1414
import aws.smithy.kotlin.runtime.http.engine.callContext
1515
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1616
import aws.smithy.kotlin.runtime.http.response.HttpCall
17+
import aws.smithy.kotlin.runtime.logging.Logger
1718
import aws.smithy.kotlin.runtime.time.Instant
1819
import kotlinx.coroutines.job
1920
import kotlinx.coroutines.sync.Mutex
2021
import kotlinx.coroutines.sync.withLock
22+
import kotlinx.coroutines.withTimeoutOrNull
23+
import kotlin.time.ExperimentalTime
2124

2225
internal const val DEFAULT_WINDOW_SIZE_BYTES: Int = 16 * 1024
2326

2427
/**
2528
* [HttpClientEngine] based on the AWS Common Runtime HTTP client
2629
*/
27-
public class CrtHttpEngine(public val config: HttpClientEngineConfig) : HttpClientEngineBase("crt") {
28-
// FIXME - use the default TLS context when profile cred provider branch is merged
29-
private val tlsCtx = TlsContext(TlsContextOptions.defaultClient())
30+
@OptIn(ExperimentalTime::class)
31+
public class CrtHttpEngine(public val config: CrtHttpEngineConfig) : HttpClientEngineBase("crt") {
32+
public constructor() : this(CrtHttpEngineConfig.Default)
3033

34+
public companion object {
35+
public operator fun invoke(block: CrtHttpEngineConfig.Builder.() -> Unit): CrtHttpEngine = CrtHttpEngine(CrtHttpEngineConfig.Builder().apply(block).build())
36+
}
37+
private val logger = Logger.getLogger<CrtHttpEngine>()
38+
39+
private val customTlsContext: TlsContext? = if (config.alpn.isNotEmpty() && config.tlsContext == null) {
40+
val options = TlsContextOptionsBuilder().apply {
41+
verifyPeer = true
42+
alpn = config.alpn.joinToString(separator = ";") { it.protocolId }
43+
}.build()
44+
TlsContext(options)
45+
} else {
46+
null
47+
}
48+
49+
init {
50+
logger.warn { "CrtHttpEngine does not support HttpClientEngineConfig.socketReadTimeout(${config.socketReadTimeout}); ignoring" }
51+
logger.warn { "CrtHttpEngine does not support HttpClientEngineConfig.socketWriteTimeout(${config.socketWriteTimeout}); ignoring" }
52+
}
53+
54+
@OptIn(ExperimentalTime::class)
3155
private val options = HttpClientConnectionManagerOptionsBuilder().apply {
32-
clientBootstrap = SdkDefaultIO.ClientBootstrap
33-
tlsContext = tlsCtx
56+
clientBootstrap = config.clientBootstrap ?: SdkDefaultIO.ClientBootstrap
57+
tlsContext = customTlsContext ?: config.tlsContext ?: SdkDefaultIO.TlsContext
3458
manualWindowManagement = true
35-
socketOptions = SocketOptions()
36-
initialWindowSize = DEFAULT_WINDOW_SIZE_BYTES
37-
// TODO - max connections/timeouts/etc
59+
socketOptions = SocketOptions(
60+
connectTimeoutMs = config.connectTimeout.inWholeMilliseconds.toInt()
61+
)
62+
initialWindowSize = config.initialWindowSizeBytes
63+
maxConnections = config.maxConnections.toInt()
64+
maxConnectionIdleMs = config.connectionIdleTimeout.inWholeMilliseconds
3865
}
3966

4067
// connection managers are per host
4168
private val connManagers = mutableMapOf<String, HttpClientConnectionManager>()
4269
private val mutex = Mutex()
4370

71+
@OptIn(ExperimentalTime::class)
4472
override suspend fun roundTrip(request: HttpRequest): HttpCall {
4573
val callContext = callContext()
4674
val manager = getManagerForUri(request.uri)
47-
val conn = manager.acquireConnection()
75+
val conn = withTimeoutOrNull(config.connectionAcquireTimeout) {
76+
manager.acquireConnection()
77+
} ?: throw ClientException("timed out waiting for an HTTP connection to be acquired from the pool")
4878

4979
try {
5080
val reqTime = Instant.now()
@@ -78,7 +108,7 @@ public class CrtHttpEngine(public val config: HttpClientEngineConfig) : HttpClie
78108
// close all resources
79109
// SAFETY: shutdown is only invoked once AND only after all requests have completed and no more are coming
80110
connManagers.forEach { entry -> entry.value.close() }
81-
tlsCtx.close()
111+
customTlsContext?.close()
82112
}
83113

84114
private suspend fun getManagerForUri(uri: Uri): HttpClientConnectionManager = mutex.withLock {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.sdk.kotlin.runtime.http.engine.crt
7+
8+
import aws.sdk.kotlin.crt.io.ClientBootstrap
9+
import aws.sdk.kotlin.crt.io.TlsContext
10+
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
11+
import aws.smithy.kotlin.runtime.util.InternalApi
12+
13+
@InternalApi
14+
public class CrtHttpEngineConfig private constructor(builder: Builder) : HttpClientEngineConfig(builder) {
15+
public companion object {
16+
/**
17+
* The default engine config. Most clients should use this.
18+
*/
19+
public val Default: CrtHttpEngineConfig = CrtHttpEngineConfig(Builder())
20+
}
21+
22+
/**
23+
* The amount of data that can be buffered before reading from the socket will cease. Reading will
24+
* resume as data is consumed.
25+
*/
26+
public val initialWindowSizeBytes: Int = builder.initialWindowSizeBytes
27+
28+
/**
29+
* The [ClientBootstrap] to use for the engine. By default it is a shared instance.
30+
*/
31+
public var clientBootstrap: ClientBootstrap? = builder.clientBootstrap
32+
33+
/**
34+
* The TLS context to use. By default it is a shared instance.
35+
*/
36+
public var tlsContext: TlsContext? = builder.tlsContext
37+
38+
public class Builder : HttpClientEngineConfig.Builder() {
39+
/**
40+
* Set the amount of data that can be buffered before reading from the socket will cease. Reading will
41+
* resume as data is consumed.
42+
*/
43+
public var initialWindowSizeBytes: Int = DEFAULT_WINDOW_SIZE_BYTES
44+
45+
/**
46+
* Set the [ClientBootstrap] to use for the engine. By default it is a shared instance.
47+
*/
48+
public var clientBootstrap: ClientBootstrap? = null
49+
50+
/**
51+
* Set the TLS context to use. By default it is a shared instance.
52+
*/
53+
public var tlsContext: TlsContext? = null
54+
55+
internal fun build(): CrtHttpEngineConfig = CrtHttpEngineConfig(this)
56+
}
57+
}

aws-runtime/http-client-engine-crt/jvm/test/aws/sdk/kotlin/runtime/http/engine/crt/AsyncStressTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package aws.sdk.kotlin.runtime.http.engine.crt
88
import aws.sdk.kotlin.runtime.testing.runSuspendTest
99
import aws.smithy.kotlin.runtime.http.HttpMethod
1010
import aws.smithy.kotlin.runtime.http.Protocol
11-
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
1211
import aws.smithy.kotlin.runtime.http.readAll
1312
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
1413
import aws.smithy.kotlin.runtime.http.request.url
@@ -43,7 +42,7 @@ class AsyncStressTest : TestWithLocalServer() {
4342
@Test
4443
fun testConcurrentRequests() = runSuspendTest {
4544
// https://github.com/awslabs/aws-sdk-kotlin/issues/170
46-
val client = sdkHttpClient(CrtHttpEngine(HttpClientEngineConfig()))
45+
val client = sdkHttpClient(CrtHttpEngine())
4746
val request = HttpRequestBuilder().apply {
4847
url {
4948
scheme = Protocol.HTTP
@@ -78,7 +77,7 @@ class AsyncStressTest : TestWithLocalServer() {
7877
// appropriately and allows requests to proceed (a stream that isn't consumed will be in a stuck state
7978
// if the window is full and never incremented again, this can lead to all connections being consumed
8079
// and the engine to no longer make further requests)
81-
val client = sdkHttpClient(CrtHttpEngine(HttpClientEngineConfig()))
80+
val client = sdkHttpClient(CrtHttpEngine())
8281
val request = HttpRequestBuilder().apply {
8382
url {
8483
scheme = Protocol.HTTP

0 commit comments

Comments
 (0)