Skip to content

Commit 4e8c875

Browse files
authored
feat: bind okhttp engine directly (#663)
Remove Ktor as a middleman between us and OkHttp. The default engine is still OkHttp based as it was before but now the integration is directly mapped to OkHttp rather than through Ktor. See also #656, #662
1 parent 2547699 commit 4e8c875

File tree

39 files changed

+1357
-91
lines changed

39 files changed

+1357
-91
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "c6d1863b-170d-4622-8900-19a75a9ebf9d",
3+
"type": "misc",
4+
"description": "Refactor to bind directly to okhttp and remove ktor as a middleman",
5+
"issues": [
6+
"awslabs/smithy-kotlin#629"
7+
]
8+
}

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ dependencies {
136136
val lintPaths = listOf(
137137
"smithy-kotlin-codegen/src/**/*.kt",
138138
"runtime/**/*.kt",
139-
"benchmarks/**/jvm/*.kt",
139+
"tests/**/jvm/**/*.kt",
140140
)
141141

142142
tasks.register<JavaExec>("ktlint") {

gradle.properties

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ dokkaVersion=1.6.21
1616

1717
# kotlin libraries
1818
coroutinesVersion=1.6.1
19-
ktorVersion=2.0.1
20-
atomicFuVersion=0.17.2
19+
ktorVersion=2.0.2
20+
atomicFuVersion=0.17.3
2121
kotlinxSerializationVersion=1.3.3
2222
jsoupVersion=1.14.3
23+
okHttpVersion=5.0.0-alpha.9
2324

2425
# codegen
2526
smithyVersion=1.17.0
@@ -43,4 +44,4 @@ kotlinLoggingVersion=2.1.21
4344
slf4jVersion=1.7.36
4445

4546
# crt
46-
crtKotlinVersion=0.6.0
47+
crtKotlinVersion=0.6.0

runtime/protocol/http-client-engines/http-client-engine-crt/common/src/aws/smithy/kotlin/runtime/http/engine/crt/SdkStreamResponseHandler.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ internal class SdkStreamResponseHandler(
7474
}
7575
}
7676

77-
private fun createHttpResponseBody(contentLength: Long): HttpBody {
77+
private fun createHttpResponseBody(contentLength: Long?): HttpBody {
7878
sdkBody = bufferedReadChannel(::onDataConsumed)
7979
return object : HttpBody.Streaming() {
80-
override val contentLength: Long = contentLength
80+
override val contentLength: Long? = contentLength
8181
override fun readFrom(): SdkByteReadChannel =
8282
sdkBody!!
8383
}
@@ -90,10 +90,10 @@ internal class SdkStreamResponseHandler(
9090

9191
val transferEncoding = headers["Transfer-Encoding"]?.lowercase()
9292
val chunked = transferEncoding == "chunked"
93-
val contentLength = headers["Content-Length"]?.toLong() ?: 0
93+
val contentLength = headers["Content-Length"]?.toLong()
9494
val status = HttpStatusCode.fromValue(stream.responseStatusCode)
9595

96-
val hasBody = (contentLength > 0 || chunked) &&
96+
val hasBody = ((contentLength != null && contentLength > 0) || chunked) &&
9797
(status !in listOf(HttpStatusCode.NotModified, HttpStatusCode.NoContent)) &&
9898
!status.isInformational()
9999

runtime/protocol/http-client-engines/http-client-engine-default/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@ kotlin {
1818
}
1919
jvmMain {
2020
dependencies {
21-
implementation(project(":runtime:protocol:http-client-engines:http-client-engine-ktor"))
2221
// okhttp works on both JVM and Android
23-
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
22+
implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp"))
2423
}
2524
}
2625
all {

runtime/protocol/http-client-engines/http-client-engine-default/common/src/aws/smithy/kotlin/runtime/http/engine/DefaultHttpEngine.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,6 @@ package aws.smithy.kotlin.runtime.http.engine
88
/**
99
* Factory function to create a new HTTP client engine using the default for the current KMP target
1010
*/
11-
fun DefaultHttpEngine(config: HttpClientEngineConfig = HttpClientEngineConfig.Default): HttpClientEngine =
12-
newDefaultHttpEngine(config)
11+
fun DefaultHttpEngine(block: (HttpClientEngineConfig.Builder.() -> Unit)? = null): HttpClientEngine = newDefaultHttpEngine(block)
1312

14-
fun DefaultHttpEngine(block: HttpClientEngineConfig.Builder.() -> Unit): HttpClientEngine {
15-
val builder = HttpClientEngineConfig.Builder().apply(block)
16-
val config = HttpClientEngineConfig(builder)
17-
return DefaultHttpEngine(config)
18-
}
19-
20-
internal expect fun newDefaultHttpEngine(config: HttpClientEngineConfig): HttpClientEngine
13+
internal expect fun newDefaultHttpEngine(block: (HttpClientEngineConfig.Builder.() -> Unit)?): HttpClientEngine

runtime/protocol/http-client-engines/http-client-engine-default/jvm/src/aws/smithy/kotlin/runtime/http/engine/DefaultHttpEngineJVM.kt

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,10 @@
55

66
package aws.smithy.kotlin.runtime.http.engine
77

8-
import aws.smithy.kotlin.runtime.http.engine.ktor.KtorEngine
9-
import io.ktor.client.engine.okhttp.*
10-
import okhttp3.ConnectionPool
11-
import okhttp3.Protocol
12-
import java.util.concurrent.TimeUnit
13-
import kotlin.time.toJavaDuration
8+
import aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine
149

15-
internal actual fun newDefaultHttpEngine(config: HttpClientEngineConfig): HttpClientEngine {
16-
val okHttpEngine = OkHttp.create {
17-
config {
18-
connectTimeout(config.connectTimeout.toJavaDuration())
19-
readTimeout(config.socketReadTimeout.toJavaDuration())
20-
writeTimeout(config.socketWriteTimeout.toJavaDuration())
21-
val pool = ConnectionPool(
22-
maxIdleConnections = config.maxConnections.toInt(),
23-
keepAliveDuration = config.connectionIdleTimeout.inWholeMilliseconds,
24-
TimeUnit.MILLISECONDS
25-
)
26-
connectionPool(pool)
27-
28-
eventListener(HttpEngineEventListener(pool))
29-
30-
if (config.alpn.isNotEmpty()) {
31-
val protocols = config.alpn.mapNotNull {
32-
when (it) {
33-
AlpnId.HTTP1_1 -> Protocol.HTTP_1_1
34-
AlpnId.HTTP2 -> Protocol.HTTP_2
35-
else -> null
36-
}
37-
}
38-
protocols(protocols)
39-
}
40-
}
41-
}
42-
43-
return KtorEngine(okHttpEngine)
10+
internal actual fun newDefaultHttpEngine(block: (HttpClientEngineConfig.Builder.() -> Unit)?): HttpClientEngine = if (block != null) {
11+
OkHttpEngine(block)
12+
} else {
13+
OkHttpEngine()
4414
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@ import aws.smithy.kotlin.runtime.client.ExecutionContext
88
import aws.smithy.kotlin.runtime.http.Headers
99
import aws.smithy.kotlin.runtime.http.HttpStatusCode
1010
import aws.smithy.kotlin.runtime.http.engine.*
11-
import aws.smithy.kotlin.runtime.http.operation.getLogger
1211
import aws.smithy.kotlin.runtime.http.operation.withContext
1312
import aws.smithy.kotlin.runtime.http.request.HttpRequest
1413
import aws.smithy.kotlin.runtime.http.response.HttpCall
1514
import aws.smithy.kotlin.runtime.http.response.HttpResponse
1615
import aws.smithy.kotlin.runtime.logging.Logger
1716
import aws.smithy.kotlin.runtime.logging.trace
1817
import aws.smithy.kotlin.runtime.time.Instant
18+
import aws.smithy.kotlin.runtime.util.InternalApi
1919
import io.ktor.client.*
2020
import io.ktor.client.call.*
2121
import io.ktor.client.request.*
22-
import io.ktor.client.statement.*
2322
import io.ktor.http.*
2423
import io.ktor.util.*
2524
import io.ktor.utils.io.*
@@ -38,6 +37,7 @@ import io.ktor.client.engine.HttpClientEngine as KtorHttpClientEngine
3837
* This class can be used to wrap any Ktor compliant engine (though not all engines
3938
* may support HTTP features required by any given SDK).
4039
*/
40+
@InternalApi // FIXME - decide on whether to support in GA or not. Most use cases would be better off by wrapping the underlying ktor engine directly
4141
class KtorEngine(
4242
private val engine: KtorHttpClientEngine
4343
) : HttpClientEngineBase("ktor") {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
description = "OkHttp Client Engine for Smithy services generated by smithy-kotlin"
7+
extra["displayName"] = "Smithy :: Kotlin :: HTTP :: Engine :: OkHttp"
8+
extra["moduleName"] = "aws.smithy.kotlin.runtime.http.engine.okhttp"
9+
10+
val coroutinesVersion: String by project
11+
val okHttpVersion: String by project
12+
13+
kotlin {
14+
sourceSets {
15+
commonMain {
16+
dependencies {
17+
api(project(":runtime:protocol:http"))
18+
implementation(project(":runtime:logging"))
19+
implementation(project(":runtime:io"))
20+
21+
implementation("com.squareup.okhttp3:okhttp:$okHttpVersion")
22+
implementation("com.squareup.okhttp3:okhttp-coroutines:$okHttpVersion")
23+
24+
}
25+
}
26+
commonTest {
27+
dependencies {
28+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
29+
implementation(project(":runtime:hashing"))
30+
}
31+
}
32+
33+
all {
34+
languageSettings.optIn("aws.smithy.kotlin.runtime.util.InternalApi")
35+
}
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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.engine.okhttp
7+
8+
import aws.smithy.kotlin.runtime.http.HttpBody
9+
import kotlinx.coroutines.CoroutineDispatcher
10+
import kotlinx.coroutines.runBlocking
11+
import okhttp3.MediaType
12+
import okhttp3.RequestBody
13+
import okio.BufferedSink
14+
import java.nio.ByteBuffer
15+
import kotlin.coroutines.CoroutineContext
16+
17+
/**
18+
* OkHttp [RequestBody] that reads from [body] channel
19+
*/
20+
internal class ByteChannelRequestBody(
21+
private val body: HttpBody.Streaming,
22+
private val callContext: CoroutineContext
23+
) : RequestBody() {
24+
override fun contentType(): MediaType? = null
25+
override fun contentLength(): Long = body.contentLength ?: -1
26+
override fun isOneShot(): Boolean = !body.isReplayable
27+
28+
// TODO - enable for event streams. Requires different processing of request body
29+
override fun isDuplex(): Boolean = false
30+
31+
@OptIn(ExperimentalStdlibApi::class)
32+
override fun writeTo(sink: BufferedSink) {
33+
// remove the current dispatcher (if it exists) and use the internal
34+
// runBlocking dispatcher that blocks the current thread
35+
val sendContext = callContext.minusKey(CoroutineDispatcher) + callContext.derivedName("send-request-body")
36+
37+
// Non-duplex (aka "normal") requests MUST write all of their request body
38+
// before this function returns. Requests are given a background thread to
39+
// do this work in, and it is safe and expected to block.
40+
// see: https://square.github.io/okhttp/4.x/okhttp/okhttp3/-request-body/is-duplex/
41+
runBlocking(sendContext) {
42+
val chan = body.readFrom()
43+
val buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE)
44+
while (!chan.isClosedForRead) {
45+
// fill the buffer by reading chunks from the underlying source
46+
while (chan.readAvailable(buffer) != -1 && buffer.remaining() > 0) {
47+
}
48+
49+
buffer.flip()
50+
while (buffer.remaining() > 0) {
51+
sink.write(buffer)
52+
}
53+
54+
buffer.clear()
55+
}
56+
}
57+
}
58+
}

0 commit comments

Comments
 (0)