diff --git a/.brazil.json b/.brazil.json index 0908ffedb4..1a0b943fec 100644 --- a/.brazil.json +++ b/.brazil.json @@ -5,6 +5,7 @@ "com.squareup.okhttp3:okhttp-coroutines:5.*": "OkHttp3Coroutines-5.x", "com.squareup.okhttp3:okhttp:5.*": "OkHttp3-5.x", + "com.squareup.okhttp3:okhttp-jvm:5.*": "OkHttp3-5.x", "com.squareup.okio:okio-jvm:3.*": "OkioJvm-3.x", "io.opentelemetry:opentelemetry-api:1.*": "Maven-io-opentelemetry_opentelemetry-api-1.x", "io.opentelemetry:opentelemetry-extension-kotlin:1.*": "Maven-io-opentelemetry_opentelemetry-extension-kotlin-1.x", diff --git a/.changes/db001c20-3788-4cfe-9ec2-284fd86a80bd.json b/.changes/db001c20-3788-4cfe-9ec2-284fd86a80bd.json new file mode 100644 index 0000000000..436c640085 --- /dev/null +++ b/.changes/db001c20-3788-4cfe-9ec2-284fd86a80bd.json @@ -0,0 +1,8 @@ +{ + "id": "db001c20-3788-4cfe-9ec2-284fd86a80bd", + "type": "bugfix", + "description": "Reimplement idle connection monitoring using `okhttp3.EventListener` instead of now-internal `okhttp3.ConnectionListener`", + "issues": [ + "https://github.com/smithy-lang/smithy-kotlin/issues/1311" + ] +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index afff864643..e6b5877dcd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,9 +7,9 @@ aws-kotlin-repo-tools-version = "0.4.31" # libs coroutines-version = "1.10.2" atomicfu-version = "0.29.0" -okhttp-version = "5.0.0-alpha.14" +okhttp-version = "5.1.0" okhttp4-version = "4.12.0" -okio-version = "3.9.1" +okio-version = "3.15.0" otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" 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 25c2339550..77e6120478 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 @@ -87,7 +87,7 @@ public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConf } 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 static final fun buildClient (Laws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngineConfig;Laws/smithy/kotlin/runtime/http/engine/internal/HttpClientMetrics;[Lokhttp3/EventListener;)Lokhttp3/OkHttpClient; } public final class aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpHeadersAdapter : aws/smithy/kotlin/runtime/http/Headers { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionIdleMonitor.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionMonitoringEventListener.kt similarity index 78% rename from runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionIdleMonitor.kt rename to runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionMonitoringEventListener.kt index 3f4c366f70..131466e66e 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionIdleMonitor.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/ConnectionMonitoringEventListener.kt @@ -4,12 +4,10 @@ */ package aws.smithy.kotlin.runtime.http.engine.okhttp +import aws.smithy.kotlin.runtime.io.Closeable import aws.smithy.kotlin.runtime.telemetry.logging.logger import kotlinx.coroutines.* -import okhttp3.Call -import okhttp3.Connection -import okhttp3.ConnectionListener -import okhttp3.ExperimentalOkHttpApi +import okhttp3.* import okhttp3.internal.closeQuietly import okio.IOException import okio.buffer @@ -22,12 +20,20 @@ import kotlin.coroutines.coroutineContext import kotlin.time.Duration import kotlin.time.measureTime -@OptIn(ExperimentalOkHttpApi::class) -internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionListener() { +/** + * An [okhttp3.EventListener] implementation that monitors connections for remote closure. + * This replaces the functionality previously provided by the now-internal [okhttp3.ConnectionListener]. + */ +internal class ConnectionMonitoringEventListener(private val pollInterval: Duration) : + EventListener(), + Closeable { private val monitorScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val monitors = ConcurrentHashMap() - fun close(): Unit = runBlocking { + /** + * Close all active connection monitors. + */ + override fun close(): Unit = runBlocking { val monitorJob = requireNotNull(monitorScope.coroutineContext[Job]) { "Connection idle monitor scope cannot be cancelled because it does not have a job: $this" } @@ -40,13 +46,16 @@ internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionLis ?.callContext ?: Dispatchers.IO - override fun connectionAcquired(connection: Connection, call: Call) { + // Cancel monitoring when a connection is acquired + override fun connectionAcquired(call: Call, connection: Connection) { + super.connectionAcquired(call, connection) + // Non-locking map access is okay here because this code will only execute synchronously as part of a // `connectionAcquired` event and will be complete before any future `connectionReleased` event could fire for // the same connection. monitors.remove(connection)?.let { monitor -> val context = call.callContext() - val logger = context.logger() + val logger = context.logger() logger.trace { "Cancel monitoring for $connection" } // Use `runBlocking` because this _must_ finish before OkHttp goes to use the connection @@ -58,13 +67,18 @@ internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionLis } } - override fun connectionReleased(connection: Connection, call: Call) { + // Start monitoring when a connection is released + override fun connectionReleased(call: Call, connection: Connection) { + super.connectionReleased(call, connection) + val connId = System.identityHashCode(connection) val callContext = call.callContext() + + // Start monitoring val monitor = monitorScope.launch(CoroutineName("okhttp-conn-monitor-for-$connId")) { doMonitor(connection, callContext) } - callContext.logger().trace { "Launched coroutine $monitor to monitor $connection" } + callContext.logger().trace { "Launched coroutine $monitor to monitor $connection" } // Non-locking map access is okay here because this code will only execute synchronously as part of a // `connectionReleased` event and will be complete before any future `connectionAcquired` event could fire for @@ -73,7 +87,7 @@ internal class ConnectionIdleMonitor(val pollInterval: Duration) : ConnectionLis } private suspend fun doMonitor(conn: Connection, callContext: CoroutineContext) { - val logger = callContext.logger() + val logger = callContext.logger() val socket = conn.socket() val source = try { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChain.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChain.kt new file mode 100644 index 0000000000..c18bd331f3 --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChain.kt @@ -0,0 +1,115 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.http.engine.okhttp + +import aws.smithy.kotlin.runtime.io.closeIfCloseable +import okhttp3.* +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy + +/** + * An [okhttp3.EventListener] that delegates to a chain of EventListeners. + * Start events are sent in forward order, terminal events are sent in reverse order + */ +internal class EventListenerChain( + private val listeners: List, +) : EventListener() { + private val reverseListeners = listeners.reversed() + + fun close() { + listeners.forEach { + it.closeIfCloseable() + } + } + + override fun callStart(call: Call): Unit = + listeners.forEach { it.callStart(call) } + + override fun dnsStart(call: Call, domainName: String): Unit = + listeners.forEach { it.dnsStart(call, domainName) } + + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List): Unit = + reverseListeners.forEach { it.dnsEnd(call, domainName, inetAddressList) } + + override fun proxySelectStart(call: Call, url: HttpUrl): Unit = + listeners.forEach { it.proxySelectStart(call, url) } + + override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List): Unit = + reverseListeners.forEach { it.proxySelectEnd(call, url, proxies) } + + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy): Unit = + listeners.forEach { it.connectStart(call, inetSocketAddress, proxy) } + + override fun secureConnectStart(call: Call): Unit = + listeners.forEach { it.secureConnectStart(call) } + + override fun secureConnectEnd(call: Call, handshake: Handshake?): Unit = + reverseListeners.forEach { it.secureConnectEnd(call, handshake) } + + override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?): Unit = + reverseListeners.forEach { it.connectEnd(call, inetSocketAddress, proxy, protocol) } + + override fun connectFailed(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException): Unit = + reverseListeners.forEach { it.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) } + + override fun connectionAcquired(call: Call, connection: Connection): Unit = + listeners.forEach { it.connectionAcquired(call, connection) } + + override fun connectionReleased(call: Call, connection: Connection): Unit = + reverseListeners.forEach { it.connectionReleased(call, connection) } + + override fun requestHeadersStart(call: Call): Unit = + listeners.forEach { it.requestHeadersStart(call) } + + override fun requestHeadersEnd(call: Call, request: Request): Unit = + reverseListeners.forEach { it.requestHeadersEnd(call, request) } + + override fun requestBodyStart(call: Call): Unit = + listeners.forEach { it.requestBodyStart(call) } + + override fun requestBodyEnd(call: Call, byteCount: Long): Unit = + reverseListeners.forEach { it.requestBodyEnd(call, byteCount) } + + override fun requestFailed(call: Call, ioe: IOException): Unit = + reverseListeners.forEach { it.requestFailed(call, ioe) } + + override fun responseHeadersStart(call: Call): Unit = + listeners.forEach { it.responseHeadersStart(call) } + + override fun responseHeadersEnd(call: Call, response: Response): Unit = + reverseListeners.forEach { it.responseHeadersEnd(call, response) } + + override fun responseBodyStart(call: Call): Unit = + listeners.forEach { it.responseBodyStart(call) } + + override fun responseBodyEnd(call: Call, byteCount: Long): Unit = + reverseListeners.forEach { it.responseBodyEnd(call, byteCount) } + + override fun responseFailed(call: Call, ioe: IOException): Unit = + reverseListeners.forEach { it.responseFailed(call, ioe) } + + override fun callEnd(call: Call): Unit = + reverseListeners.forEach { it.callEnd(call) } + + override fun callFailed(call: Call, ioe: IOException): Unit = + reverseListeners.forEach { it.callFailed(call, ioe) } + + override fun canceled(call: Call): Unit = + reverseListeners.forEach { it.canceled(call) } + + override fun satisfactionFailure(call: Call, response: Response): Unit = + reverseListeners.forEach { it.satisfactionFailure(call, response) } + + override fun cacheConditionalHit(call: Call, cachedResponse: Response): Unit = + listeners.forEach { it.cacheConditionalHit(call, cachedResponse) } + + override fun cacheHit(call: Call, response: Response): Unit = + listeners.forEach { it.cacheHit(call, response) } + + override fun cacheMiss(call: Call): Unit = + listeners.forEach { it.cacheMiss(call) } +} 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 a20387d0a0..c8f8fde7d5 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 @@ -11,6 +11,7 @@ import aws.smithy.kotlin.runtime.http.config.EngineFactory import aws.smithy.kotlin.runtime.http.engine.* import aws.smithy.kotlin.runtime.http.engine.internal.HttpClientMetrics import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.io.closeIfCloseable import aws.smithy.kotlin.runtime.net.TlsVersion import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.time.Instant @@ -44,9 +45,14 @@ public class OkHttpEngine( override val engineConstructor: (OkHttpEngineConfig.Builder.() -> Unit) -> OkHttpEngine = ::invoke } + // Create a single shared connection monitoring listener if idle polling is enabled + private val connectionMonitoringListener: EventListener? = + config.connectionIdlePollingInterval?.let { + ConnectionMonitoringEventListener(it) + } + private val metrics = HttpClientMetrics(TELEMETRY_SCOPE, config.telemetryProvider) - private val connectionIdleMonitor = config.connectionIdlePollingInterval?.let { ConnectionIdleMonitor(it) } - private val client = config.buildClientWithConnectionListener(metrics, connectionIdleMonitor) + private val client = config.buildClient(metrics, connectionMonitoringListener) @OptIn(ExperimentalCoroutinesApi::class) override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { @@ -73,16 +79,20 @@ public class OkHttpEngine( } override fun shutdown() { - connectionIdleMonitor?.close() + connectionMonitoringListener?.closeIfCloseable() client.connectionPool.evictAll() client.dispatcher.executorService.shutdown() metrics.close() } } -private fun OkHttpEngineConfig.buildClientFromConfig( +/** + * Convert SDK version of HTTP configuration to OkHttp specific configuration and return the configured client + */ +@InternalApi +public fun OkHttpEngineConfig.buildClient( metrics: HttpClientMetrics, - poolOverride: ConnectionPool? = null, + vararg clientScopedEventListeners: EventListener?, ): OkHttpClient { val config = this @@ -102,7 +112,7 @@ private fun OkHttpEngineConfig.buildClientFromConfig( writeTimeout(config.socketWriteTimeout.toJavaDuration()) // use our own pool configured with the timeout settings taken from config - val pool = poolOverride ?: ConnectionPool( + val pool = ConnectionPool( maxIdleConnections = 5, // The default from the no-arg ConnectionPool() constructor keepAliveDuration = config.connectionIdleTimeout.inWholeMilliseconds, TimeUnit.MILLISECONDS, @@ -116,7 +126,14 @@ private fun OkHttpEngineConfig.buildClientFromConfig( dispatcher(dispatcher) // Log events coming from okhttp. Allocate a new listener per-call to facilitate dedicated trace spans. - eventListenerFactory { call -> HttpEngineEventListener(pool, config.hostResolver, dispatcher, metrics, call) } + eventListenerFactory { call -> + EventListenerChain( + listOfNotNull( + HttpEngineEventListener(pool, config.hostResolver, dispatcher, metrics, call), + *clientScopedEventListeners, + ), + ) + } // map protocols if (config.tlsContext.alpn.isNotEmpty()) { @@ -140,34 +157,6 @@ private fun OkHttpEngineConfig.buildClientFromConfig( }.build() } -/** - * Convert SDK version of HTTP configuration to OkHttp specific configuration and return the configured client - */ -// Used by OkHttp4Engine - OkHttp4 does NOT have `connectionListener` -// TODO - Refactor in next minor version - Move this to OkHttp4Engine and make it private -@InternalApi -public fun OkHttpEngineConfig.buildClient( - metrics: HttpClientMetrics, -): OkHttpClient = this.buildClientFromConfig(metrics) - -/** - * Convert SDK version of HTTP configuration to OkHttp specific configuration and return the configured client - */ -// Used by OkHttpEngine - OkHttp5 does have `connectionListener` -@OptIn(ExperimentalOkHttpApi::class) -private fun OkHttpEngineConfig.buildClientWithConnectionListener( - metrics: HttpClientMetrics, - connectionListener: ConnectionIdleMonitor?, -): OkHttpClient = this.buildClientFromConfig( - metrics, - ConnectionPool( - maxIdleConnections = 5, // The default from the no-arg ConnectionPool() constructor - keepAliveDuration = this.connectionIdleTimeout.inWholeMilliseconds, - timeUnit = TimeUnit.MILLISECONDS, - connectionListener = connectionListener ?: ConnectionListener.NONE, - ), -) - private fun minTlsConnectionSpec(tlsContext: TlsContext): ConnectionSpec { val minVersion = tlsContext.minVersion ?: TlsVersion.TLS_1_2 val okHttpTlsVersions = SdkTlsVersion diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/test/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChainTest.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/test/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChainTest.kt new file mode 100644 index 0000000000..c9acb07cef --- /dev/null +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/test/aws/smithy/kotlin/runtime/http/engine/okhttp/EventListenerChainTest.kt @@ -0,0 +1,281 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.http.engine.okhttp + +import okhttp3.* +import java.io.Closeable +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class EventListenerChainTest { + @Test + fun testForwardEvents() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + val call = createMockCall() + + // Test forward events + chain.callStart(call) + chain.dnsStart(call, "example.com") + chain.proxySelectStart(call, createHttpUrl()) + + // Verify forward events were called in order (listener1 first, then listener2) + assertEquals("listener1:callStart", eventOrder[0]) + assertEquals("listener2:callStart", eventOrder[1]) + assertEquals("listener1:dnsStart", eventOrder[2]) + assertEquals("listener2:dnsStart", eventOrder[3]) + assertEquals("listener1:proxySelectStart", eventOrder[4]) + assertEquals("listener2:proxySelectStart", eventOrder[5]) + } + + @Test + fun testReverseEvents() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + val call = createMockCall() + + // Test reverse events + chain.dnsEnd(call, "example.com", listOf()) + chain.proxySelectEnd(call, createHttpUrl(), listOf()) + chain.callEnd(call) + + // Verify reverse events were called in reverse order (listener2 first, then listener1) + assertEquals("listener2:dnsEnd", eventOrder[0]) + assertEquals("listener1:dnsEnd", eventOrder[1]) + assertEquals("listener2:proxySelectEnd", eventOrder[2]) + assertEquals("listener1:proxySelectEnd", eventOrder[3]) + assertEquals("listener2:callEnd", eventOrder[4]) + assertEquals("listener1:callEnd", eventOrder[5]) + } + + @Test + fun testClose() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + // Close the chain + chain.close() + + // Verify all listeners were closed + assertTrue(listener1.closed) + assertTrue(listener2.closed) + } + + @Test + fun testMixedEvents() { + val eventOrder = mutableListOf() + + val listener1 = TestEventListener("listener1", eventOrder) + val listener2 = TestEventListener("listener2", eventOrder) + + val chain = EventListenerChain(listOf(listener1, listener2)) + + val call = createMockCall() + + // Test mixed forward and reverse events + chain.callStart(call) + chain.dnsStart(call, "example.com") + chain.dnsEnd(call, "example.com", listOf()) + + // Verify the order of events + assertEquals("listener1:callStart", eventOrder[0]) // listener1 first (forward) + assertEquals("listener2:callStart", eventOrder[1]) // listener2 second (forward) + assertEquals("listener1:dnsStart", eventOrder[2]) // listener1 first (forward) + assertEquals("listener2:dnsStart", eventOrder[3]) // listener2 second (forward) + assertEquals("listener2:dnsEnd", eventOrder[4]) // listener2 first (reverse) + assertEquals("listener1:dnsEnd", eventOrder[5]) // listener1 second (reverse) + + // Clear event order + eventOrder.clear() + + // Test more events to verify the sequence + chain.requestHeadersStart(call) // forward event + chain.requestHeadersEnd(call, Request.Builder().url("https://example.com").build()) // reverse event + chain.responseHeadersStart(call) // forward event + chain.responseHeadersEnd( + call, + Response.Builder() + .request(Request.Builder().url("https://example.com").build()) + .protocol(Protocol.HTTP_2) + .code(200) + .message("OK") + .build(), + ) // reverse event + + // Verify the sequence of events + assertEquals("listener1:requestHeadersStart", eventOrder[0]) // listener1 first (forward) + assertEquals("listener2:requestHeadersStart", eventOrder[1]) // listener2 second (forward) + assertEquals("listener2:requestHeadersEnd", eventOrder[2]) // listener2 first (reverse) + assertEquals("listener1:requestHeadersEnd", eventOrder[3]) // listener1 second (reverse) + assertEquals("listener1:responseHeadersStart", eventOrder[4]) // listener1 first (forward) + assertEquals("listener2:responseHeadersStart", eventOrder[5]) // listener2 second (forward) + assertEquals("listener2:responseHeadersEnd", eventOrder[6]) // listener2 first (reverse) + assertEquals("listener1:responseHeadersEnd", eventOrder[7]) // listener1 second (reverse) + } + + // A test EventListener that records the order of calls + private class TestEventListener(val name: String, val eventOrder: MutableList) : + EventListener(), + Closeable { + var closed = false + + override fun callStart(call: Call) { + eventOrder.add("$name:callStart") + } + + override fun dnsStart(call: Call, domainName: String) { + eventOrder.add("$name:dnsStart") + } + + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { + eventOrder.add("$name:dnsEnd") + } + + override fun proxySelectStart(call: Call, url: HttpUrl) { + eventOrder.add("$name:proxySelectStart") + } + + override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List) { + eventOrder.add("$name:proxySelectEnd") + } + + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { + eventOrder.add("$name:connectStart") + } + + override fun secureConnectStart(call: Call) { + eventOrder.add("$name:secureConnectStart") + } + + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + eventOrder.add("$name:secureConnectEnd") + } + + override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) { + eventOrder.add("$name:connectEnd") + } + + override fun connectFailed(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?, ioe: IOException) { + eventOrder.add("$name:connectFailed") + } + + override fun connectionAcquired(call: Call, connection: Connection) { + eventOrder.add("$name:connectionAcquired") + } + + override fun connectionReleased(call: Call, connection: Connection) { + eventOrder.add("$name:connectionReleased") + } + + override fun requestHeadersStart(call: Call) { + eventOrder.add("$name:requestHeadersStart") + } + + override fun requestHeadersEnd(call: Call, request: Request) { + eventOrder.add("$name:requestHeadersEnd") + } + + override fun requestBodyStart(call: Call) { + eventOrder.add("$name:requestBodyStart") + } + + override fun requestBodyEnd(call: Call, byteCount: Long) { + eventOrder.add("$name:requestBodyEnd") + } + + override fun requestFailed(call: Call, ioe: IOException) { + eventOrder.add("$name:requestFailed") + } + + override fun responseHeadersStart(call: Call) { + eventOrder.add("$name:responseHeadersStart") + } + + override fun responseHeadersEnd(call: Call, response: Response) { + eventOrder.add("$name:responseHeadersEnd") + } + + override fun responseBodyStart(call: Call) { + eventOrder.add("$name:responseBodyStart") + } + + override fun responseBodyEnd(call: Call, byteCount: Long) { + eventOrder.add("$name:responseBodyEnd") + } + + override fun responseFailed(call: Call, ioe: IOException) { + eventOrder.add("$name:responseFailed") + } + + override fun callEnd(call: Call) { + eventOrder.add("$name:callEnd") + } + + override fun callFailed(call: Call, ioe: IOException) { + eventOrder.add("$name:callFailed") + } + + override fun canceled(call: Call) { + eventOrder.add("$name:canceled") + } + + override fun satisfactionFailure(call: Call, response: Response) { + eventOrder.add("$name:satisfactionFailure") + } + + override fun cacheConditionalHit(call: Call, cachedResponse: Response) { + eventOrder.add("$name:cacheConditionalHit") + } + + override fun cacheHit(call: Call, response: Response) { + eventOrder.add("$name:cacheHit") + } + + override fun cacheMiss(call: Call) { + eventOrder.add("$name:cacheMiss") + } + + override fun close() { + closed = true + } + } + + // Helper methods to create mock objects + private fun createMockCall(): Call = object : Call { + override fun cancel() {} + override fun clone(): Call = this + override fun enqueue(responseCallback: Callback) {} + override fun execute(): Response = throw UnsupportedOperationException() + override fun isCanceled(): Boolean = false + override fun isExecuted(): Boolean = false + override fun request(): Request = Request.Builder().url("https://example.com").build() + override fun timeout(): okio.Timeout = okio.Timeout() + } + + private fun createHttpUrl(): HttpUrl = HttpUrl.Builder() + .scheme("https") + .host("example.com") + .build() +}