From cbf98f7990d5ab709a318a660583c1025b788534 Mon Sep 17 00:00:00 2001 From: Matas Date: Fri, 13 Sep 2024 15:57:22 -0400 Subject: [PATCH] feat: OkHttp4Engine (#1150) --- .../7acc674e-0dfe-461a-a5c3-f946e14a3ec7.json | 5 + gradle/libs.versions.toml | 2 + .../api/http-client-engine-okhttp.api | 99 +++++++++++++++++ .../engine/okhttp/HttpEngineEventListener.kt | 48 +++++---- .../http/engine/okhttp/MetricsInterceptor.kt | 12 ++- .../http/engine/okhttp/OkHttpEngine.kt | 6 +- .../engine/okhttp/OkHttpHeadersAdapter.kt | 4 +- .../runtime/http/engine/okhttp/OkHttpUtils.kt | 45 +++++--- .../engine/okhttp/StreamingRequestBody.kt | 4 +- .../http-client-engine-okhttp4/README.md | 91 ++++++++++++++++ .../api/http-client-engine-okhttp4.api | 14 +++ .../build.gradle.kts | 30 ++++++ .../http/engine/okhttp4/OkHttp4Engine.kt | 102 ++++++++++++++++++ .../test-suite/build.gradle.kts | 2 + .../http/test/util/AbstractEngineTestJVM.kt | 2 + settings.gradle.kts | 1 + tests/benchmarks/http-benchmarks/README.md | 32 +++++- .../http-benchmarks/build.gradle.kts | 1 + .../benchmarks/http/HttpEngineBenchmarks.kt | 5 +- 19 files changed, 454 insertions(+), 51 deletions(-) create mode 100644 .changes/7acc674e-0dfe-461a-a5c3-f946e14a3ec7.json create mode 100644 runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md create mode 100644 runtime/protocol/http-client-engines/http-client-engine-okhttp4/api/http-client-engine-okhttp4.api create mode 100644 runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts create mode 100644 runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt diff --git a/.changes/7acc674e-0dfe-461a-a5c3-f946e14a3ec7.json b/.changes/7acc674e-0dfe-461a-a5c3-f946e14a3ec7.json new file mode 100644 index 000000000..61e07280b --- /dev/null +++ b/.changes/7acc674e-0dfe-461a-a5c3-f946e14a3ec7.json @@ -0,0 +1,5 @@ +{ + "id": "7acc674e-0dfe-461a-a5c3-f946e14a3ec7", + "type": "feature", + "description": "Add OkHttp4Engine, an HTTP engine which uses okhttp3:4.x" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 784dd6e0a..6f8da9016 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ aws-kotlin-repo-tools-version = "0.4.10" coroutines-version = "1.8.1" atomicfu-version = "0.24.0" okhttp-version = "5.0.0-alpha.14" +okhttp4-version = "4.12.0" okio-version = "3.9.0" otel-version = "1.32.0" slf4j-version = "2.0.9" @@ -51,6 +52,7 @@ kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines- okio = { module = "com.squareup.okio:okio", version.ref = "okio-version" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp-version" } +okhttp4 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp4-version" } okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "okhttp-version" } opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api", version.ref = "otel-version" } opentelemetry-sdk-testing = {module = "io.opentelemetry:opentelemetry-sdk-testing", version.ref = "otel-version" } diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api b/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api index e78c3e539..3ab63989e 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/api/http-client-engine-okhttp.api @@ -1,3 +1,54 @@ +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/HttpEngineEventListener : okhttp3/EventListener { + public fun (Lokhttp3/ConnectionPool;Laws/smithy/kotlin/runtime/net/HostResolver;Lokhttp3/Dispatcher;Laws/smithy/kotlin/runtime/http/engine/internal/HttpClientMetrics;Lokhttp3/Call;)V + public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheMiss (Lokhttp3/Call;)V + public fun callEnd (Lokhttp3/Call;)V + public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun callStart (Lokhttp3/Call;)V + public fun canceled (Lokhttp3/Call;)V + public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V + public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V + public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V + public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V + public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V + public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V + public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V + public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V + public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V + public fun requestBodyEnd (Lokhttp3/Call;J)V + public fun requestBodyStart (Lokhttp3/Call;)V + public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V + public fun requestHeadersStart (Lokhttp3/Call;)V + public fun responseBodyEnd (Lokhttp3/Call;J)V + public fun responseBodyStart (Lokhttp3/Call;)V + public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V + public fun responseHeadersStart (Lokhttp3/Call;)V + public fun satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V + public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V + public fun secureConnectStart (Lokhttp3/Call;)V +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor : okhttp3/Interceptor { + public static final field INSTANCE Laws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor; + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpCall : aws/smithy/kotlin/runtime/http/HttpCall { + public fun (Laws/smithy/kotlin/runtime/http/request/HttpRequest;Laws/smithy/kotlin/runtime/http/response/HttpResponse;Laws/smithy/kotlin/runtime/time/Instant;Laws/smithy/kotlin/runtime/time/Instant;Lkotlin/coroutines/CoroutineContext;Lokhttp3/Call;)V + public synthetic fun (Laws/smithy/kotlin/runtime/http/request/HttpRequest;Laws/smithy/kotlin/runtime/http/response/HttpResponse;Laws/smithy/kotlin/runtime/time/Instant;Laws/smithy/kotlin/runtime/time/Instant;Lkotlin/coroutines/CoroutineContext;Lokhttp3/Call;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun cancelInFlight ()V + public fun copy (Laws/smithy/kotlin/runtime/http/request/HttpRequest;Laws/smithy/kotlin/runtime/http/response/HttpResponse;)Laws/smithy/kotlin/runtime/http/HttpCall; + public final fun getCall ()Lokhttp3/Call; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpDns : okhttp3/Dns { + public fun (Laws/smithy/kotlin/runtime/net/HostResolver;)V + public fun lookup (Ljava/lang/String;)Ljava/util/List; +} + public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine : aws/smithy/kotlin/runtime/http/engine/HttpClientEngineBase { public static final field Companion Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine$Companion; public fun ()V @@ -32,3 +83,51 @@ public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConf public final fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConfig; } +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineKt { + public static final fun buildClient (Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConfig;Laws/smithy/kotlin/runtime/http/engine/internal/HttpClientMetrics;)Lokhttp3/OkHttpClient; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpHeadersAdapter : aws/smithy/kotlin/runtime/http/Headers { + public fun (Lokhttp3/Headers;)V + public fun contains (Ljava/lang/String;)Z + public synthetic fun contains (Ljava/lang/String;Ljava/lang/Object;)Z + public fun contains (Ljava/lang/String;Ljava/lang/String;)Z + public fun entries ()Ljava/util/Set; + public fun forEach (Lkotlin/jvm/functions/Function2;)V + public synthetic fun get (Ljava/lang/String;)Ljava/lang/Object; + public fun get (Ljava/lang/String;)Ljava/lang/String; + public fun getAll (Ljava/lang/String;)Ljava/util/List; + public fun getCaseInsensitiveName ()Z + public fun isEmpty ()Z + public fun names ()Ljava/util/Set; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpProxyAuthenticator : okhttp3/Authenticator { + public fun (Laws/smithy/kotlin/runtime/http/engine/ProxySelector;)V + public fun authenticate (Lokhttp3/Route;Lokhttp3/Response;)Lokhttp3/Request; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpProxySelector : java/net/ProxySelector { + public fun (Laws/smithy/kotlin/runtime/http/engine/ProxySelector;)V + public fun connectFailed (Ljava/net/URI;Ljava/net/SocketAddress;Ljava/io/IOException;)V + public fun select (Ljava/net/URI;)Ljava/util/List; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtilsKt { + public static final fun errCode (Ljava/lang/Exception;)Laws/smithy/kotlin/runtime/http/HttpErrorCode; + public static final fun mapOkHttpExceptions (Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; + public static final fun toOkHttpHeaders (Laws/smithy/kotlin/runtime/http/Headers;)Lokhttp3/Headers; + public static final fun toOkHttpRequest (Laws/smithy/kotlin/runtime/http/request/HttpRequest;Laws/smithy/kotlin/runtime/operation/ExecutionContext;Lkotlin/coroutines/CoroutineContext;Laws/smithy/kotlin/runtime/http/engine/internal/HttpClientMetrics;)Lokhttp3/Request; + public static final fun toSdkResponse (Lokhttp3/Response;)Laws/smithy/kotlin/runtime/http/response/HttpResponse; + public static final fun toUrl (Ljava/net/URI;)Laws/smithy/kotlin/runtime/net/url/Url; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp/StreamingRequestBody : okhttp3/RequestBody { + public fun (Laws/smithy/kotlin/runtime/http/HttpBody;Lkotlin/coroutines/CoroutineContext;)V + public fun contentLength ()J + public fun contentType ()Lokhttp3/MediaType; + public fun isDuplex ()Z + public fun isOneShot ()Z + public fun writeTo (Lokio/BufferedSink;)V +} + diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/HttpEngineEventListener.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/HttpEngineEventListener.kt index a34f4bbc4..492650f74 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/HttpEngineEventListener.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/HttpEngineEventListener.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.engine.okhttp import aws.smithy.kotlin.runtime.ExperimentalApi +import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.http.engine.EngineAttributes import aws.smithy.kotlin.runtime.http.engine.internal.HttpClientMetrics import aws.smithy.kotlin.runtime.net.HostResolver @@ -31,19 +32,20 @@ internal const val TELEMETRY_SCOPE = "aws.smithy.kotlin.runtime.http.engine.okht // see https://square.github.io/okhttp/features/events/#eventlistener for example callback flow @OptIn(ExperimentalApi::class) -internal class HttpEngineEventListener( +@InternalApi +public class HttpEngineEventListener( private val pool: ConnectionPool, private val hr: HostResolver, private val dispatcher: Dispatcher, private val metrics: HttpClientMetrics, call: Call, ) : EventListener() { - private val provider: TelemetryProvider = call.request().tag()?.callContext?.telemetryProvider ?: TelemetryProvider.None + private val provider: TelemetryProvider = call.request().tag(SdkRequestTag::class.java)?.callContext?.telemetryProvider ?: TelemetryProvider.None private val traceSpan = provider.tracerProvider .getOrCreateTracer(TELEMETRY_SCOPE) .createSpan("HTTP") - private val logger = call.request().tag()?.callContext?.logger() ?: LoggerProvider.None.getLogger() + private val logger = call.request().tag(SdkRequestTag::class.java)?.callContext?.logger() ?: LoggerProvider.None.getLogger() // callStart() is invoked immediately when enqueued, next success phase is either dnsStart() or connectionAcquired() // see https://github.com/square/okhttp/blob/7c92ed0879477eddb2fce6b4066d151525d5687f/okhttp/src/jvmMain/kotlin/okhttp3/internal/connection/RealCall.kt#L167-L175 @@ -84,22 +86,22 @@ internal class HttpEngineEventListener( trace { "dns query: domain=$domainName" } } - override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) = + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List): Unit = trace { "dns resolved: domain=$domainName; records=$inetAddressList" } - override fun proxySelectStart(call: Call, url: HttpUrl) = trace { "proxy select start: url=$url" } + override fun proxySelectStart(call: Call, url: HttpUrl): Unit = trace { "proxy select start: url=$url" } - override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List) = + override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List): Unit = trace { "proxy select end: url=$url; proxies=$proxies" } - override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) = + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy): Unit = trace { "starting connection: addr=$inetSocketAddress; proxy=$proxy" } - override fun secureConnectStart(call: Call) = trace { "initiating TLS connection" } + override fun secureConnectStart(call: Call): Unit = trace { "initiating TLS connection" } - override fun secureConnectEnd(call: Call, handshake: Handshake?) = trace { "TLS connect end: handshake=$handshake" } + override fun secureConnectEnd(call: Call, handshake: Handshake?): Unit = trace { "TLS connect end: handshake=$handshake" } - override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) = + override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?): Unit = trace { "connection established: addr=$inetSocketAddress; proxy=$proxy; protocol=$protocol" } override fun connectFailed( @@ -139,7 +141,7 @@ internal class HttpEngineEventListener( trace { "connection acquired: conn(id=$connId)=$connection; connPool: total=${pool.connectionCount()}, idle=${pool.idleConnectionCount()}" } } - override fun requestHeadersStart(call: Call) = trace { "sending request headers" } + override fun requestHeadersStart(call: Call): Unit = trace { "sending request headers" } override fun requestHeadersEnd(call: Call, request: Request) { if (request.body == null) { @@ -149,34 +151,34 @@ internal class HttpEngineEventListener( trace { "finished sending request headers" } } - override fun requestBodyStart(call: Call) = trace { "sending request body" } + override fun requestBodyStart(call: Call): Unit = trace { "sending request body" } override fun requestBodyEnd(call: Call, byteCount: Long) { requestTimeEnd = TimeSource.Monotonic.markNow() trace { "finished sending request body: bytesSent=$byteCount" } } - override fun requestFailed(call: Call, ioe: IOException) = trace(ioe) { "request failed" } + override fun requestFailed(call: Call, ioe: IOException): Unit = trace(ioe) { "request failed" } override fun responseHeadersStart(call: Call) { requestTimeEnd?.elapsedNow()?.let { ttfb -> metrics.timeToFirstByteDuration.recordSeconds(ttfb) - call.request().tag()?.execContext?.set(EngineAttributes.TimeToFirstByte, ttfb) + call.request().tag(SdkRequestTag::class.java)?.execContext?.set(EngineAttributes.TimeToFirstByte, ttfb) } trace { "response headers start" } } override fun responseHeadersEnd(call: Call, response: Response) { - val contentLength = response.body.contentLength() + val contentLength = response.body?.contentLength() trace { "response headers end: contentLengthHeader=$contentLength" } } - override fun responseBodyStart(call: Call) = trace { "response body available" } + override fun responseBodyStart(call: Call): Unit = trace { "response body available" } - override fun responseBodyEnd(call: Call, byteCount: Long) = + override fun responseBodyEnd(call: Call, byteCount: Long): Unit = trace { "response body finished: bytesConsumed=$byteCount" } - override fun responseFailed(call: Call, ioe: IOException) = trace(ioe) { "response failed" } + override fun responseFailed(call: Call, ioe: IOException): Unit = trace(ioe) { "response failed" } override fun connectionReleased(call: Call, connection: Connection) { metrics.acquiredConnections = pool.connectionCount().toLong() @@ -201,16 +203,16 @@ internal class HttpEngineEventListener( traceSpan.close() } - override fun canceled(call: Call) = trace { "call cancelled" } + override fun canceled(call: Call): Unit = trace { "call cancelled" } // NOTE: we don't configure a cache and should never get the rest of these events, // seeing these messages logged means we configured something wrong - override fun satisfactionFailure(call: Call, response: Response) = trace { "cache satisfaction failure" } + override fun satisfactionFailure(call: Call, response: Response): Unit = trace { "cache satisfaction failure" } - override fun cacheConditionalHit(call: Call, cachedResponse: Response) = trace { "cache conditional hit" } + override fun cacheConditionalHit(call: Call, cachedResponse: Response): Unit = trace { "cache conditional hit" } - override fun cacheHit(call: Call, response: Response) = trace { "cache hit" } + override fun cacheHit(call: Call, response: Response): Unit = trace { "cache hit" } - override fun cacheMiss(call: Call) = trace { "cache miss" } + override fun cacheMiss(call: Call): Unit = trace { "cache miss" } } diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt index 718b30017..0ea8ae5e6 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt @@ -4,6 +4,7 @@ */ package aws.smithy.kotlin.runtime.http.engine.okhttp +import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.collections.attributesOf import aws.smithy.kotlin.runtime.telemetry.metrics.MonotonicCounter @@ -13,10 +14,11 @@ import okio.* /** * Instrument the HTTP throughput metrics (e.g. bytes rcvd/sent) */ -internal object MetricsInterceptor : Interceptor { +@InternalApi +public object MetricsInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() - val metrics = originalRequest.tag()?.metrics ?: return chain.proceed(originalRequest) + val metrics = originalRequest.tag(SdkRequestTag::class.java)?.metrics ?: return chain.proceed(originalRequest) val attrs = attributesOf { "server.address" to "${originalRequest.url.host}:${originalRequest.url.port}" } val request = if (originalRequest.body != null) { @@ -28,12 +30,12 @@ internal object MetricsInterceptor : Interceptor { } val originalResponse = chain.proceed(request) - val response = if (originalResponse.body.contentLength() != 0L) { + val response = if (originalResponse.body == null || originalResponse.body?.contentLength() == 0L) { + originalResponse + } else { originalResponse.newBuilder() .body(originalResponse.body.instrument(metrics.bytesReceived, attrs)) .build() - } else { - originalResponse } return response diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt index 263468d3b..ba50e7c51 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.engine.okhttp +import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.http.HttpCall import aws.smithy.kotlin.runtime.http.config.EngineFactory import aws.smithy.kotlin.runtime.http.engine.* @@ -64,7 +65,7 @@ public class OkHttpEngine( // else). In both cases we need to ensure that the engine-side resources are cleaned up completely // since they wouldn't otherwise be. https://github.com/smithy-lang/smithy-kotlin/issues/1061 if (cause != null) call.cancelInFlight() - engineResponse.body.close() + engineResponse.body?.close() } } } @@ -79,7 +80,8 @@ public class OkHttpEngine( /** * Convert SDK version of HTTP configuration to OkHttp specific configuration and return the configured client */ -private fun OkHttpEngineConfig.buildClient(metrics: HttpClientMetrics): OkHttpClient { +@InternalApi +public fun OkHttpEngineConfig.buildClient(metrics: HttpClientMetrics): OkHttpClient { val config = this return OkHttpClient.Builder().apply { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpHeadersAdapter.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpHeadersAdapter.kt index 9f0cf8c4b..d3a24525f 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpHeadersAdapter.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpHeadersAdapter.kt @@ -5,13 +5,15 @@ package aws.smithy.kotlin.runtime.http.engine.okhttp +import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.http.Headers as SdkHeaders import okhttp3.Headers as OkHttpHeaders /** * Proxy [okhttp3.Headers] as [aws.smithy.kotlin.runtime.http.Headers] */ -internal class OkHttpHeadersAdapter(private val headers: OkHttpHeaders) : SdkHeaders { +@InternalApi +public class OkHttpHeadersAdapter(private val headers: OkHttpHeaders) : SdkHeaders { override val caseInsensitiveName: Boolean = true override fun getAll(name: String): List? = diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt index 76a60540a..329897967 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.engine.okhttp +import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.http.* import aws.smithy.kotlin.runtime.http.HttpCall import aws.smithy.kotlin.runtime.http.engine.ProxyConfig @@ -42,13 +43,14 @@ internal data class SdkRequestTag(val execContext: ExecutionContext, val callCon /** * Convert SDK [HttpRequest] to an [okhttp3.Request] instance */ -internal fun HttpRequest.toOkHttpRequest( +@InternalApi +public fun HttpRequest.toOkHttpRequest( execContext: ExecutionContext, callContext: CoroutineContext, metrics: HttpClientMetrics, ): OkHttpRequest { val builder = OkHttpRequest.Builder() - builder.tag(SdkRequestTag::class, SdkRequestTag(execContext, callContext, metrics)) + builder.tag(SdkRequestTag::class.java, SdkRequestTag(execContext, callContext, metrics)) builder.url(url.toString()) builder.headers(headers.toOkHttpHeaders()) @@ -82,7 +84,8 @@ internal fun HttpRequest.toOkHttpRequest( return builder.build() } -private fun Headers.toOkHttpHeaders(): OkHttpHeaders = OkHttpHeaders.Builder().also { okHeaders -> +@InternalApi +public fun Headers.toOkHttpHeaders(): OkHttpHeaders = OkHttpHeaders.Builder().also { okHeaders -> forEach { key, values -> values.forEach { value -> okHeaders.addUnsafeNonAscii(key, value) @@ -98,24 +101,26 @@ private fun Headers.toOkHttpHeaders(): OkHttpHeaders = OkHttpHeaders.Builder().a /** * Convert an [okhttp3.Response] to an SDK [HttpResponse] */ -internal fun OkHttpResponse.toSdkResponse(): HttpResponse { +@InternalApi +public fun OkHttpResponse.toSdkResponse(): HttpResponse { val sdkHeaders = OkHttpHeadersAdapter(headers) - val httpBody = if (body.contentLength() != 0L) { + val httpBody = if (body == null || body!!.contentLength() == 0L) { + HttpBody.Empty + } else { object : HttpBody.SourceContent() { override val isOneShot: Boolean = true // -1 is used by okhttp as transfer-encoding chunked - override val contentLength: Long? = if (body.contentLength() >= 0L) body.contentLength() else null - override fun readFrom(): SdkSource = body.source().toSdk() + override val contentLength: Long? = if (body!!.contentLength() >= 0L) body!!.contentLength() else null + override fun readFrom(): SdkSource = body!!.source().toSdk() } - } else { - HttpBody.Empty } return HttpResponse(HttpStatusCode.fromValue(code), sdkHeaders, httpBody) } -internal class OkHttpProxyAuthenticator( +@InternalApi +public class OkHttpProxyAuthenticator( private val selector: SdkProxySelector, ) : Authenticator { override fun authenticate(route: Route?, response: okhttp3.Response): okhttp3.Request? { @@ -156,7 +161,8 @@ internal class OkHttpProxyAuthenticator( } } -internal class OkHttpDns( +@InternalApi +public class OkHttpDns( private val hr: HostResolver, ) : Dns { // we assume OkHttp is calling us on an IO thread already @@ -166,7 +172,8 @@ internal class OkHttpDns( } } -internal class OkHttpProxySelector( +@InternalApi +public class OkHttpProxySelector( private val sdkSelector: SdkProxySelector, ) : ProxySelector() { override fun select(uri: URI?): List { @@ -184,13 +191,14 @@ internal class OkHttpProxySelector( override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {} } -internal class OkHttpCall( +@InternalApi +public class OkHttpCall( request: HttpRequest, response: HttpResponse, requestTime: Instant, responseTime: Instant, coroutineContext: CoroutineContext = EmptyCoroutineContext, - val call: Call, + public val call: Call, ) : HttpCall(request, response, requestTime, responseTime, coroutineContext) { override fun copy(request: HttpRequest, response: HttpResponse): HttpCall = OkHttpCall(request, response, requestTime, responseTime, coroutineContext, call) @@ -200,7 +208,8 @@ internal class OkHttpCall( } } -private fun URI.toUrl(): Url { +@InternalApi +public fun URI.toUrl(): Url { val uri = this return Url { scheme = Scheme.parse(uri.scheme) @@ -223,14 +232,16 @@ private fun URI.toUrl(): Url { } } -internal inline fun mapOkHttpExceptions(block: () -> T): T = +@InternalApi +public inline fun mapOkHttpExceptions(block: () -> T): T = try { block() } catch (ex: IOException) { throw HttpException(ex, ex.errCode(), retryable = true) // All IOExceptions are retryable } -private fun Exception.errCode(): HttpErrorCode = when { +@InternalApi +public fun Exception.errCode(): HttpErrorCode = when { isConnectTimeoutException() -> HttpErrorCode.CONNECT_TIMEOUT isConnectionClosedException() -> HttpErrorCode.CONNECTION_CLOSED isCauseOrSuppressed() -> HttpErrorCode.SOCKET_TIMEOUT diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/StreamingRequestBody.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/StreamingRequestBody.kt index aa174228b..c48875e5e 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/StreamingRequestBody.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/StreamingRequestBody.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.engine.okhttp +import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.io.internal.toOkio import aws.smithy.kotlin.runtime.io.internal.toSdk @@ -22,7 +23,8 @@ import kotlin.coroutines.CoroutineContext * OkHttp [RequestBody] that reads from [body] channel or source */ @OptIn(DelicateCoroutinesApi::class, ExperimentalStdlibApi::class) -internal class StreamingRequestBody( +@InternalApi +public class StreamingRequestBody( private val body: HttpBody, private val callContext: CoroutineContext, ) : RequestBody() { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md new file mode 100644 index 000000000..0f7077a21 --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/README.md @@ -0,0 +1,91 @@ +# OkHttp4 Engine + +The AWS SDK for Kotlin depends on OkHttp **5.0.0-alpha.x**, which despite being in alpha, is claimed to be production stable and safe for consumption: + +> Although this release is labeled alpha, the only unstable thing in it is our new APIs. +> This release has many critical bug fixes and is safe to run in production. +> We’re eager to stabilize our new APIs so we can get out of alpha. +> +> https://square.github.io/okhttp/changelogs/changelog/#version-500-alpha12 + +This `OkHttp4Engine` is intended to be used for applications which still depend on okhttp3 **4.x** and can't upgrade to the newest alpha version. + +## Configuration + +### Gradle +Because the SDK's default HTTP engine depends on okhttp3 **5.0.0-alpha.X**, consumers will need to force Gradle to resolve to **4.x** to prevent the alpha dependency from being introduced transitively. Here is a sample configuration: +```kts +dependencies { + implementation("aws.sdk.kotlin:s3:$SDK_VERSION") // and any other AWS SDK clients... + implementation("aws.smithy.kotlin:http-client-engine-okhttp4:$SMITHY_KOTLIN_VERSION") // depend on OkHttp4Engine +} + +configurations.all { + resolutionStrategy { + // Force resolve to OkHttp 4.x + force("com.squareup.okhttp3:okhttp:4.12.0") // or whichever version you are using... + } + exclude(group = "com.squareup.okhttp3", module = "okhttp-coroutines") // Exclude dependency on okhttp-coroutines, which is introduced in 5.0.0-alpha.X +} +``` + +### AWS SDK for Kotlin +You will also need to configure your SDK's HTTP client to use the `OkHttp4Engine`: +```kt +import aws.sdk.kotlin.services.s3.* +import aws.smithy.kotlin.runtime.http.engine.okhttp4.OkHttp4Engine +import kotlinx.coroutines.runBlocking + +fun main() = runBlocking { + OkHttp4Engine().use { okHttp4Engine -> + S3Client.fromEnvironment { + httpClient = okHttp4Engine + }.use { + // use the client! + } + } +} +``` + +For more tips on configuring the HTTP client, [see our developer guide entry](https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/http-client-config.html). + +## Troubleshooting + +### java.lang.NoClassDefFoundError +If you see an exception similar to this... +``` +java.lang.NoClassDefFoundError: okhttp3/coroutines/ExecuteAsyncKt + at aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine.roundTrip(OkHttpEngine.kt:56) ~[http-client-engine-okhttp-jvm-1.3.9-SNAPSHOT.jar:?] + at aws.smithy.kotlin.runtime.http.engine.internal.ManagedHttpClientEngine.roundTrip(ManagedHttpClientEngine.kt) ~[http-client-jvm-1.3.9-SNAPSHOT.jar:?] + at aws.smithy.kotlin.runtime.http.SdkHttpClient$executeWithCallContext$2.invokeSuspend(SdkHttpClient.kt:44) ~[http-client-jvm-1.3.9-SNAPSHOT.jar:?] + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) ~[kotlin-stdlib-2.0.10.jar:2.0.10-release-540] + at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104) ~[kotlinx-coroutines-core-jvm-1.8.1.jar:?] + at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584) ~[kotlinx-coroutines-core-jvm-1.8.1.jar:?] + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811) ~[kotlinx-coroutines-core-jvm-1.8.1.jar:?] + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715) ~[kotlinx-coroutines-core-jvm-1.8.1.jar:?] + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702) ~[kotlinx-coroutines-core-jvm-1.8.1.jar:?] +Caused by: java.lang.ClassNotFoundException: okhttp3.coroutines.ExecuteAsyncKt + at jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[?:?] + at jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[?:?] + at java.lang.ClassLoader.loadClass(ClassLoader.java:525) ~[?:?] + ... 9 more +Exception in thread "main" java.lang.NoClassDefFoundError: okhttp3/coroutines/ExecuteAsyncKt + at aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine.roundTrip(OkHttpEngine.kt:56) + at aws.smithy.kotlin.runtime.http.engine.internal.ManagedHttpClientEngine.roundTrip(ManagedHttpClientEngine.kt) + at aws.smithy.kotlin.runtime.http.SdkHttpClient$executeWithCallContext$2.invokeSuspend(SdkHttpClient.kt:44) + at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) + at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104) + at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584) + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:811) + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:715) + at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:702) +Caused by: java.lang.ClassNotFoundException: okhttp3.coroutines.ExecuteAsyncKt + at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) + at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) + at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:525) + ... 9 more +``` + +It likely means you failed to configure the SDK client to use the `OkHttpEngine4`. +Please double-check all of your SDK client configurations to ensure `httpClient = OkHttpEngine4()` is configured, +and if the problem persists, [open an issue](https://github.com/smithy-lang/smithy-kotlin/issues/new/choose). \ No newline at end of file diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/api/http-client-engine-okhttp4.api b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/api/http-client-engine-okhttp4.api new file mode 100644 index 000000000..af5656600 --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/api/http-client-engine-okhttp4.api @@ -0,0 +1,14 @@ +public final class aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine : aws/smithy/kotlin/runtime/http/engine/HttpClientEngineBase { + public static final field Companion Laws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine$Companion; + public fun ()V + public fun (Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConfig;)V + public synthetic fun getConfig ()Laws/smithy/kotlin/runtime/http/engine/HttpClientEngineConfig; + public fun getConfig ()Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConfig; + public fun roundTrip (Laws/smithy/kotlin/runtime/operation/ExecutionContext;Laws/smithy/kotlin/runtime/http/request/HttpRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine$Companion : aws/smithy/kotlin/runtime/http/config/EngineFactory { + public fun getEngineConstructor ()Lkotlin/jvm/functions/Function1; + public final fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine; +} + diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts new file mode 100644 index 000000000..26145b406 --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +description = "OkHttp 4.x Client Engine for Smithy services generated by smithy-kotlin" +extra["displayName"] = "Smithy :: Kotlin :: HTTP :: Engine :: OkHttp4" +extra["moduleName"] = "aws.smithy.kotlin.runtime.http.engine.okhttp4" + +kotlin { + sourceSets { + commonMain { + dependencies { + api(project(":runtime:protocol:http-client")) + implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp")) + implementation(libs.okhttp4) + } + } + + all { + languageSettings.optIn("aws.smithy.kotlin.runtime.InternalApi") + } + } +} + +configurations.all { + resolutionStrategy { + force(libs.okhttp4) + } +} diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt new file mode 100644 index 000000000..422d3952c --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.http.engine.okhttp4 + +import aws.smithy.kotlin.runtime.http.HttpCall +import aws.smithy.kotlin.runtime.http.config.EngineFactory +import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase +import aws.smithy.kotlin.runtime.http.engine.callContext +import aws.smithy.kotlin.runtime.http.engine.internal.HttpClientMetrics +import aws.smithy.kotlin.runtime.http.engine.okhttp.* +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.operation.ExecutionContext +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.job +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.* +import okhttp3.internal.closeQuietly +import okio.IOException +import kotlin.coroutines.resumeWithException + +internal const val TELEMETRY_SCOPE = "aws.smithy.kotlin.runtime.http.engine.okhttp4" + +/** + * [aws.smithy.kotlin.runtime.http.engine.HttpClientEngine] based on OkHttp3-4.x. + */ +public class OkHttp4Engine( + override val config: OkHttpEngineConfig, +) : HttpClientEngineBase("OkHttp4") { + public constructor() : this(OkHttpEngineConfig.Default) + + public companion object : EngineFactory { + /** + * Initializes a new [OkHttp4Engine] via a DSL builder block + * @param block A receiver lambda which sets the properties of the config to be built + */ + public operator fun invoke(block: OkHttpEngineConfig.Builder.() -> Unit): OkHttp4Engine = + OkHttp4Engine(OkHttpEngineConfig(block)) + + override val engineConstructor: (OkHttpEngineConfig.Builder.() -> Unit) -> OkHttp4Engine = ::invoke + } + + private val metrics = HttpClientMetrics(TELEMETRY_SCOPE, config.telemetryProvider) + private val client = config.buildClient(metrics) + + override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { + val callContext = callContext() + + val engineRequest = request.toOkHttpRequest(context, callContext, metrics) + val engineCall = client.newCall(engineRequest) + + @OptIn(ExperimentalCoroutinesApi::class) + val engineResponse = mapOkHttpExceptions { engineCall.executeAsync() } + + val response = engineResponse.toSdkResponse() + val requestTime = Instant.fromEpochMilliseconds(engineResponse.sentRequestAtMillis) + val responseTime = Instant.fromEpochMilliseconds(engineResponse.receivedResponseAtMillis) + + return OkHttpCall(request, response, requestTime, responseTime, callContext, engineCall).also { call -> + callContext.job.invokeOnCompletion { cause -> + // If cause is non-null that means the job was cancelled (CancellationException) or failed (anything + // else). In both cases we need to ensure that the engine-side resources are cleaned up completely + // since they wouldn't otherwise be. https://github.com/smithy-lang/smithy-kotlin/issues/1061 + if (cause != null) call.cancelInFlight() + engineResponse.body?.close() + } + } + } +} + +// Copied from okhttp3 5.x: +// https://github.com/square/okhttp/blob/d58da0a65b7f9cdbdf25b198e804153164ae729f/okhttp-coroutines/src/main/kotlin/okhttp3/coroutines/ExecuteAsync.kt +@ExperimentalCoroutinesApi // resume with a resource cleanup. +private suspend fun Call.executeAsync(): Response = + suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + this.cancel() + } + this.enqueue( + object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + continuation.resumeWithException(e) + } + + override fun onResponse( + call: Call, + response: Response, + ) { + continuation.resume(response) { + response.closeQuietly() + } + } + }, + ) + } diff --git a/runtime/protocol/http-client-engines/test-suite/build.gradle.kts b/runtime/protocol/http-client-engines/test-suite/build.gradle.kts index 04b846dbc..10edd6851 100644 --- a/runtime/protocol/http-client-engines/test-suite/build.gradle.kts +++ b/runtime/protocol/http-client-engines/test-suite/build.gradle.kts @@ -29,6 +29,8 @@ kotlin { implementation(project(":runtime:protocol:http-client-engines:http-client-engine-default")) implementation(project(":runtime:protocol:http-client-engines:http-client-engine-crt")) + implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp")) + implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp4")) implementation(libs.slf4j.simple) } diff --git a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt b/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt index f18ed8399..fa091c1f0 100644 --- a/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt +++ b/runtime/protocol/http-client-engines/test-suite/jvm/src/aws/smithy/kotlin/runtime/http/test/util/AbstractEngineTestJVM.kt @@ -7,12 +7,14 @@ package aws.smithy.kotlin.runtime.http.test.util import aws.smithy.kotlin.runtime.http.engine.DefaultHttpEngine import aws.smithy.kotlin.runtime.http.engine.crt.CrtHttpEngine +import aws.smithy.kotlin.runtime.http.engine.okhttp4.OkHttp4Engine import aws.smithy.kotlin.runtime.net.url.Url internal actual fun engineFactories(): List = listOf( TestEngineFactory("DefaultHttpEngine", ::DefaultHttpEngine), TestEngineFactory("CrtHttpEngine") { CrtHttpEngine(it) }, + TestEngineFactory("OkHttp4Engine") { OkHttp4Engine(it) }, ) internal actual val testServers = mapOf( diff --git a/settings.gradle.kts b/settings.gradle.kts index c80670eb6..414df686a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,6 +57,7 @@ include(":runtime:protocol:http-client") include(":runtime:protocol:http-client-engines:http-client-engine-crt") include(":runtime:protocol:http-client-engines:http-client-engine-default") include(":runtime:protocol:http-client-engines:http-client-engine-okhttp") +include(":runtime:protocol:http-client-engines:http-client-engine-okhttp4") include(":runtime:protocol:http-client-engines:test-suite") include(":runtime:protocol:http-test") include(":runtime:runtime-core") diff --git a/tests/benchmarks/http-benchmarks/README.md b/tests/benchmarks/http-benchmarks/README.md index 7705b87ac..77a6a7787 100644 --- a/tests/benchmarks/http-benchmarks/README.md +++ b/tests/benchmarks/http-benchmarks/README.md @@ -8,10 +8,40 @@ This project contains benchmarks for the [HTTP engine implementations](../../../ ./gradlew :tests:benchmarks:http-benchmarks:benchmark ``` -Baseline `0.14.0-SNAPSHOT` on EC2 **[m5.4xlarge](https://aws.amazon.com/ec2/instance-types/m5/)** with **Corretto-11.0.15.9.1**: +## Results + +All tests are run on EC2 m5.4xlarge unless specified otherwise. The download/upload throughput benchmarks are an approximation of how much data in MB/s we are able to process. +### 1.3.9 +- Added OkHttp4 engine + +``` +jvm summary: +Benchmark (httpClientName) Mode Cnt Score Error Units +HttpEngineBenchmarks.downloadThroughputNoTls OkHttp thrpt 5 745.301 ± 35.401 ops/s +HttpEngineBenchmarks.downloadThroughputNoTls CRT thrpt 5 378.639 ± 31.692 ops/s +HttpEngineBenchmarks.downloadThroughputNoTls OkHttp4 thrpt 5 751.228 ± 20.876 ops/s +HttpEngineBenchmarks.roundTripConcurrentNoTls OkHttp thrpt 5 22678.327 ± 358.711 ops/s +HttpEngineBenchmarks.roundTripConcurrentNoTls CRT thrpt 5 19444.576 ± 1766.956 ops/s +HttpEngineBenchmarks.roundTripConcurrentNoTls OkHttp4 thrpt 5 23325.643 ± 212.193 ops/s +HttpEngineBenchmarks.roundTripSequentialNoTls OkHttp thrpt 5 6370.241 ± 851.269 ops/s +HttpEngineBenchmarks.roundTripSequentialNoTls CRT thrpt 5 6024.056 ± 829.415 ops/s +HttpEngineBenchmarks.roundTripSequentialNoTls OkHttp4 thrpt 5 6510.030 ± 464.146 ops/s +HttpEngineBenchmarks.uploadThroughputChannelContentNoTls OkHttp thrpt 5 189.346 ± 5.934 ops/s +HttpEngineBenchmarks.uploadThroughputChannelContentNoTls CRT thrpt 5 116.265 ± 0.240 ops/s +HttpEngineBenchmarks.uploadThroughputChannelContentNoTls OkHttp4 thrpt 5 189.269 ± 6.007 ops/s +HttpEngineBenchmarks.uploadThroughputNoTls OkHttp thrpt 5 188.174 ± 1.866 ops/s +HttpEngineBenchmarks.uploadThroughputNoTls CRT thrpt 5 197.143 ± 2.890 ops/s +HttpEngineBenchmarks.uploadThroughputNoTls OkHttp4 thrpt 5 189.736 ± 3.535 ops/s +HttpEngineBenchmarks.uploadThroughputSourceContentNoTls OkHttp thrpt 5 197.732 ± 4.069 ops/s +HttpEngineBenchmarks.uploadThroughputSourceContentNoTls CRT thrpt 5 198.890 ± 1.889 ops/s +HttpEngineBenchmarks.uploadThroughputSourceContentNoTls OkHttp4 thrpt 5 195.378 ± 2.165 ops/s +``` + +### 0.14.0-SNAPSHOT + ``` jvm summary: Benchmark (httpClientName) Mode Cnt Score Error Units diff --git a/tests/benchmarks/http-benchmarks/build.gradle.kts b/tests/benchmarks/http-benchmarks/build.gradle.kts index 9941851e2..a91852e30 100644 --- a/tests/benchmarks/http-benchmarks/build.gradle.kts +++ b/tests/benchmarks/http-benchmarks/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { val jvmMain by getting { dependencies { implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp")) + implementation(project(":runtime:protocol:http-client-engines:http-client-engine-okhttp4")) implementation(project(":runtime:protocol:http-client-engines:http-client-engine-crt")) // mock/embedded server diff --git a/tests/benchmarks/http-benchmarks/jvm/src/aws/smithy/kotlin/benchmarks/http/HttpEngineBenchmarks.kt b/tests/benchmarks/http-benchmarks/jvm/src/aws/smithy/kotlin/benchmarks/http/HttpEngineBenchmarks.kt index 0dfe3161f..532feb487 100644 --- a/tests/benchmarks/http-benchmarks/jvm/src/aws/smithy/kotlin/benchmarks/http/HttpEngineBenchmarks.kt +++ b/tests/benchmarks/http-benchmarks/jvm/src/aws/smithy/kotlin/benchmarks/http/HttpEngineBenchmarks.kt @@ -10,6 +10,7 @@ import aws.smithy.kotlin.runtime.http.complete import aws.smithy.kotlin.runtime.http.engine.CloseableHttpClientEngine import aws.smithy.kotlin.runtime.http.engine.crt.CrtHttpEngine import aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine +import aws.smithy.kotlin.runtime.http.engine.okhttp4.OkHttp4Engine import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.request.headers import aws.smithy.kotlin.runtime.http.request.url @@ -37,6 +38,7 @@ private const val MB_PER_THROUGHPUT_OP = 12 // TODO - add TLS tests to benchmarks (or just move existing tests to use TLS since we expect that to be the norm) private const val OKHTTP_ENGINE = "OkHttp" private const val CRT_ENGINE = "CRT" +private const val OKHTTP4_ENGINE = "OkHttp4" fun interface BenchmarkEngineFactory { fun create(): CloseableHttpClientEngine @@ -45,6 +47,7 @@ fun interface BenchmarkEngineFactory { private val engines = mapOf( OKHTTP_ENGINE to BenchmarkEngineFactory { OkHttpEngine() }, CRT_ENGINE to BenchmarkEngineFactory { CrtHttpEngine() }, + OKHTTP4_ENGINE to BenchmarkEngineFactory { OkHttp4Engine() }, ) // 12MB @@ -54,7 +57,7 @@ private val largeData = ByteArray(MB_PER_THROUGHPUT_OP * 1024 * 1024) @State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.SECONDS) open class HttpEngineBenchmarks { - @Param(OKHTTP_ENGINE, CRT_ENGINE) + @Param(OKHTTP_ENGINE, CRT_ENGINE, OKHTTP4_ENGINE) var httpClientName: String = "" lateinit var engine: CloseableHttpClientEngine