Skip to content

Commit e7ee957

Browse files
authored
feat(rt): http client engine config (#493)
1 parent a07096c commit e7ee957

File tree

8 files changed

+172
-37
lines changed

8 files changed

+172
-37
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

runtime/protocol/http-client-engines/http-client-engine-ktor/jvm/src/aws/smithy/kotlin/runtime/http/engine/ktor/KtorEngine.kt

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ package aws.smithy.kotlin.runtime.http.engine.ktor
66

77
import aws.smithy.kotlin.runtime.http.Headers
88
import aws.smithy.kotlin.runtime.http.HttpStatusCode
9-
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
10-
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase
11-
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineConfig
12-
import aws.smithy.kotlin.runtime.http.engine.callContext
9+
import aws.smithy.kotlin.runtime.http.engine.*
1310
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1411
import aws.smithy.kotlin.runtime.http.response.HttpCall
1512
import aws.smithy.kotlin.runtime.logging.*
@@ -24,15 +21,46 @@ import kotlinx.coroutines.channels.Channel
2421
import kotlinx.coroutines.channels.SendChannel
2522
import kotlinx.coroutines.job
2623
import kotlinx.coroutines.launch
24+
import okhttp3.ConnectionPool
25+
import okhttp3.Protocol
26+
import java.util.concurrent.TimeUnit
2727
import kotlin.coroutines.CoroutineContext
28+
import kotlin.time.ExperimentalTime
29+
import kotlin.time.toJavaDuration
2830
import aws.smithy.kotlin.runtime.http.response.HttpResponse as SdkHttpResponse
2931

3032
/**
3133
* JVM [HttpClientEngine] backed by Ktor
3234
*/
33-
class KtorEngine(val config: HttpClientEngineConfig) : HttpClientEngineBase("ktor") {
35+
class KtorEngine(
36+
private val config: HttpClientEngineConfig = HttpClientEngineConfig.Default
37+
) : HttpClientEngineBase("ktor-okhttp") {
38+
@OptIn(ExperimentalTime::class)
3439
val client: HttpClient = HttpClient(OkHttp) {
35-
// TODO - propagate applicable client engine config to OkHttp engine
40+
engine {
41+
config {
42+
connectTimeout(config.connectTimeout.toJavaDuration())
43+
readTimeout(config.socketReadTimeout.toJavaDuration())
44+
writeTimeout(config.socketWriteTimeout.toJavaDuration())
45+
val pool = ConnectionPool(
46+
maxIdleConnections = config.maxConnections.toInt(),
47+
keepAliveDuration = config.connectionIdleTimeout.inWholeMilliseconds,
48+
TimeUnit.MILLISECONDS
49+
)
50+
connectionPool(pool)
51+
52+
if (config.alpn.isNotEmpty()) {
53+
val protocols = config.alpn.mapNotNull {
54+
when (it) {
55+
AlpnId.HTTP1_1 -> Protocol.HTTP_1_1
56+
AlpnId.HTTP2 -> Protocol.HTTP_2
57+
else -> null
58+
}
59+
}
60+
protocols(protocols)
61+
}
62+
}
63+
}
3664

3765
// do not throw exceptions if status code < 300, error handling is expected by generated clients
3866
expectSuccess = false

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

Lines changed: 0 additions & 17 deletions
This file was deleted.

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

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import aws.smithy.kotlin.runtime.http.engine.HttpClientEngine
88
import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineClosedException
99
import aws.smithy.kotlin.runtime.http.engine.SdkRequestContextElement
1010
import aws.smithy.kotlin.runtime.http.engine.createCallContext
11+
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1112
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
1213
import aws.smithy.kotlin.runtime.http.response.HttpCall
1314
import aws.smithy.kotlin.runtime.io.Handler
@@ -21,15 +22,10 @@ typealias HttpHandler = Handler<HttpRequestBuilder, HttpCall>
2122
* Create an [SdkHttpClient] with the given engine, and optionally configure it
2223
* This will **not** manage the engine lifetime, the caller is expected to close it.
2324
*/
24-
@HttpClientDsl
2525
fun sdkHttpClient(
2626
engine: HttpClientEngine,
27-
configure: HttpClientConfig.() -> Unit = {},
2827
manageEngine: Boolean = false,
29-
): SdkHttpClient {
30-
val config = HttpClientConfig().apply(configure)
31-
return SdkHttpClient(engine, config, manageEngine)
32-
}
28+
): SdkHttpClient = SdkHttpClient(engine, manageEngine)
3329

3430
/**
3531
* An HTTP client capable of round tripping requests and responses
@@ -38,19 +34,20 @@ fun sdkHttpClient(
3834
*/
3935
class SdkHttpClient(
4036
val engine: HttpClientEngine,
41-
val config: HttpClientConfig,
4237
private val manageEngine: Boolean = false
4338
) : HttpHandler {
4439
private val closed = atomic(false)
4540

46-
override suspend fun call(request: HttpRequestBuilder): HttpCall = executeWithCallContext(request)
41+
suspend fun call(request: HttpRequest): HttpCall = executeWithCallContext(request)
42+
override suspend fun call(request: HttpRequestBuilder): HttpCall = executeWithCallContext(request.build())
4743

48-
private suspend fun executeWithCallContext(request: HttpRequestBuilder): HttpCall {
44+
// FIXME - can we relocate to engine?
45+
private suspend fun executeWithCallContext(request: HttpRequest): HttpCall {
4946
if (!engine.coroutineContext.job.isActive) throw HttpClientEngineClosedException()
5047
val callContext = engine.createCallContext(coroutineContext)
5148
val context = callContext + SdkRequestContextElement(callContext)
5249
return withContext(context) {
53-
engine.roundTrip(request.build())
50+
engine.roundTrip(request)
5451
}
5552
}
5653

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngine.kt

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

77
import aws.smithy.kotlin.runtime.ClientException
88
import aws.smithy.kotlin.runtime.http.request.HttpRequest
9+
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
910
import aws.smithy.kotlin.runtime.http.response.HttpCall
1011
import aws.smithy.kotlin.runtime.io.Closeable
1112
import aws.smithy.kotlin.runtime.util.InternalApi
@@ -23,6 +24,12 @@ interface HttpClientEngine : Closeable, CoroutineScope {
2324
*/
2425
suspend fun roundTrip(request: HttpRequest): HttpCall
2526

27+
/**
28+
* Execute a single HTTP request and return the response
29+
* Consumers *MUST* call `HttpCall.complete()` when finished processing the response
30+
*/
31+
suspend fun roundTrip(request: HttpRequestBuilder): HttpCall = roundTrip(request.build())
32+
2633
/**
2734
* Shutdown and cleanup any resources
2835
*/

runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/engine/HttpClientEngineConfig.kt

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

7+
import kotlin.time.Duration
8+
import kotlin.time.ExperimentalTime
9+
10+
// See https://github.com/aws/aws-sdk-java-v2/blob/master/http-client-spi/src/main/java/software/amazon/awssdk/http/SdkHttpConfigurationOption.java
11+
// for all the options the Java v2 SDK supports
12+
713
/**
814
* Common configuration options to be interpreted by an underlying engine
15+
*
16+
* NOTE: Not all engines will support every option! Engines *SHOULD* log a warning when given a configuration
17+
* option they don't understand/support
918
*/
10-
class HttpClientEngineConfig
19+
@OptIn(ExperimentalTime::class)
20+
open class HttpClientEngineConfig constructor(builder: Builder) {
21+
constructor() : this(Builder())
22+
23+
companion object {
24+
operator fun invoke(block: Builder.() -> Unit): HttpClientEngineConfig = HttpClientEngineConfig(Builder().apply(block))
25+
26+
/**
27+
* Default client engine config
28+
*/
29+
val Default: HttpClientEngineConfig = HttpClientEngineConfig(Builder())
30+
}
31+
32+
/**
33+
* Timeout for each read to an underlying socket
34+
*/
35+
val socketReadTimeout: Duration = builder.socketReadTimeout
36+
37+
/**
38+
* Timeout for each write to an underlying socket
39+
*/
40+
val socketWriteTimeout: Duration = builder.socketWriteTimeout
41+
42+
/**
43+
* Maximum number of open connections
44+
*/
45+
val maxConnections: UInt = builder.maxConnections
46+
47+
/**
48+
* The amount of time to wait for a connection to be established
49+
*/
50+
val connectTimeout: Duration = builder.connectTimeout
51+
52+
/**
53+
* The amount of time to wait for an already-established connection from a connection pool
54+
*/
55+
val connectionAcquireTimeout: Duration = builder.connectionAcquireTimeout
56+
57+
/**
58+
* The amount of time before an idle connection should be reaped from a connection pool. Zero indicates that
59+
* idle connections should never be reaped.
60+
*/
61+
val connectionIdleTimeout: Duration = builder.connectionIdleTimeout
62+
63+
/**
64+
* Set the ALPN protocol list when a TLS connection starts
65+
*/
66+
val alpn: List<AlpnId> = builder.alpn
67+
68+
open class Builder {
69+
/**
70+
* Timeout for each read to an underlying socket
71+
*/
72+
var socketReadTimeout: Duration = Duration.seconds(30)
73+
74+
/**
75+
* Timeout for each write to an underlying socket
76+
*/
77+
var socketWriteTimeout: Duration = Duration.seconds(30)
78+
79+
/**
80+
* Maximum number of open connections
81+
*/
82+
var maxConnections: UInt = 16u
83+
84+
/**
85+
* The amount of time to wait for a connection to be established
86+
*/
87+
var connectTimeout: Duration = Duration.seconds(2)
88+
89+
/**
90+
* The amount of time to wait for an already-established connection from a connection pool
91+
*/
92+
var connectionAcquireTimeout: Duration = Duration.seconds(10)
93+
94+
/**
95+
* The amount of time before an idle connection should be reaped from a connection pool. Zero indicates that
96+
* idle connections should never be reaped.
97+
*/
98+
var connectionIdleTimeout: Duration = Duration.seconds(60)
99+
100+
/**
101+
* Set the ALPN protocol list when a TLS connection starts
102+
*/
103+
var alpn: List<AlpnId> = emptyList()
104+
}
105+
}
106+
107+
/**
108+
* Common ALPN identifiers
109+
* See the [IANA registry](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
110+
*/
111+
enum class AlpnId(val protocolId: String) {
112+
/**
113+
* HTTP 1.1
114+
*/
115+
HTTP1_1("http/1.1"),
116+
117+
/**
118+
* HTTP 2 over TLS
119+
*/
120+
HTTP2("h2"),
121+
122+
/**
123+
* HTTP 3
124+
*/
125+
HTTP3("h3")
126+
}

smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ abstract class HttpProtocolClientGenerator(
9898
}
9999

100100
// defaults to Ktor since it's the only available engine in smithy-kotlin runtime
101+
/**
102+
* The client engine to default to when one is not given in config. This type *MUST* be default constructable
103+
* or else you need to override [renderInit] and construct it manually
104+
*/
101105
protected open val defaultHttpClientEngineSymbol: Symbol = buildSymbol {
102106
name = "KtorEngine"
103107
namespace(KotlinDependency.HTTP_KTOR_ENGINE)
@@ -109,7 +113,7 @@ abstract class HttpProtocolClientGenerator(
109113
protected open fun renderInit(writer: KotlinWriter) {
110114
writer.addImport(defaultHttpClientEngineSymbol)
111115
writer.openBlock("init {", "}") {
112-
writer.write("val httpClientEngine = config.httpClientEngine ?: #T(HttpClientEngineConfig())", defaultHttpClientEngineSymbol)
116+
writer.write("val httpClientEngine = config.httpClientEngine ?: #T()", defaultHttpClientEngineSymbol)
113117
writer.write("client = sdkHttpClient(httpClientEngine, manageEngine = config.httpClientEngine == null)")
114118
}
115119
}

smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGeneratorTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class HttpProtocolClientGeneratorTest {
5252
commonTestContents.shouldContainOnlyOnceWithDiff("val client: SdkHttpClient")
5353
val expected = """
5454
init {
55-
val httpClientEngine = config.httpClientEngine ?: KtorEngine(HttpClientEngineConfig())
55+
val httpClientEngine = config.httpClientEngine ?: KtorEngine()
5656
client = sdkHttpClient(httpClientEngine, manageEngine = config.httpClientEngine == null)
5757
}
5858
"""

0 commit comments

Comments
 (0)