Skip to content

fix: Use OkHttp EventLister instead of ConnectionListener for idle connection monitoring #1312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 8, 2025

Conversation

lauzadis
Copy link
Contributor

@lauzadis lauzadis commented Jul 2, 2025

We implemented a ConnectionIdleMonitor in #1171, but it used okhttp3.ConnectionListener which was experimental. Now that API is internal in the latest release of OkHttp, we need to migrate to a stable API: okhttp3.EventListener

Issue #

Addresses #1311 in v1.5.x

Description of changes

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@lauzadis lauzadis changed the base branch from main to v1.5-main July 2, 2025 18:18

This comment has been minimized.

@lauzadis lauzadis force-pushed the fix-connection-listener branch from eb1006f to 52d84fd Compare July 2, 2025 18:20
@lauzadis lauzadis marked this pull request as ready for review July 2, 2025 18:20
@lauzadis lauzadis requested a review from a team as a code owner July 2, 2025 18:20
@lauzadis lauzadis added the acknowledge-api-break Acknowledge that a change is API breaking and may be backwards-incompatible. Review carefully! label Jul 2, 2025

This comment has been minimized.

2 similar comments

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

1 similar comment

This comment has been minimized.

@lauzadis
Copy link
Contributor Author

lauzadis commented Jul 2, 2025

smithy-client-jvm.jar increased by 7KB likely due to OkHttp version bump

This comment has been minimized.

{
"id": "db001c20-3788-4cfe-9ec2-284fd86a80bd",
"type": "bugfix",
"description": "Reimplement idle connection monitoring using okhttp3.EventListener instead of now-internal okhttp3.ConnectionListener",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: monospace class names and packages

Comment on lines 121 to 135
// If connection idle polling is enabled, use ConnectionMonitoringEventListener
eventListenerFactory { call ->
if (config.connectionIdlePollingInterval != null) {
ConnectionMonitoringEventListener(
pool,
config.hostResolver,
dispatcher,
metrics,
config.connectionIdlePollingInterval!!,
call,
)
} else {
HttpEngineEventListener(pool, config.hostResolver, dispatcher, metrics, call)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: Won't this install a new connection monitor for every HTTP call? We need our monitoring to be cross-call since the problems occur when one call has released a connection, it's closed remotely, and then a new call acquires the connection. I think we need to instantiate our monitor once (probably along with the underlying OkHttp client instance) and reuse it.

Comment on lines 28 to 35
internal class ConnectionMonitoringEventListener(
pool: ConnectionPool,
hr: HostResolver,
dispatcher: Dispatcher,
metrics: HttpClientMetrics,
private val pollInterval: Duration,
call: Call,
) : HttpEngineEventListener(pool, hr, dispatcher, metrics, call) {
Copy link
Contributor

@ianbotsf ianbotsf Jul 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness: I don't believe inheritance is the right decoupling strategy here. There's nothing really tying together the responsibilities of connection idle polling and emitting call-specific events/metrics.

I recommend an EventListener chain:

class EventListenerChain(val listeners: List<EventListener>) : EventListener() {
    val reverseListeners = listeners.asReverse()

    override fun callStart(call: Call) = listeners.forEach { it.callStart(call) }
    override fun callEnd(call: Call) = reverseListeners.forEach { it.calEnd(call) }
}

And then combine the event emitter and idle poller via a chain.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

Comment on lines 23 to 28
/**
* An [okhttp3.EventListener] implementation that monitors connections for remote closure.
* This replaces the functionality previously provided by the now-internal [okhttp3.ConnectionListener].
*/
@InternalApi
public class ConnectionMonitoringEventListener(private val pollInterval: Duration) : EventListener() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why does this need to be @InternalApi public? Isn't it only used in this module?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can't be internal because it's exposed by a public API OkHttpEngineConfig.buildClient

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does buildClient need to take a ConnectionMonitoringEventListener? Couldn't it just take a vararg clientScopedEventListeners: EventListener and make it more reusable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that'll work better, updated!

Comment on lines 28 to 30
private val monitors = ConcurrentHashMap<Connection, Job>()
private val monitors = ConcurrentHashMap<Int, Job>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why did this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤷‍♂️ no particular reason, we don't need the Connection key and we were mixing use of connection and connection ID in the last implementation, I standardized everything on connection ID

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted back to Connection since it works better when logging Connection instead of connId

Comment on lines 49 to 60
val logger = context.logger<ConnectionIdleMonitor>()
logger.trace { "Cancel monitoring for $connection" }
val logger = context.logger<ConnectionMonitoringEventListener>()
logger.trace { "Cancel monitoring for $connId" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Connection has a nice toString representation that makes logs pretty useful. Example:

Connection{s3.us-west-2.amazonaws.com:443, proxy=DIRECT hostAddress=s3.us-west-2.amazonaws.com/52.92.164.216:443 cipherSuite=TLS_AES_128_GCM_SHA256 protocol=http/1.1}

Why did we switch to logging a hashcode instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that'll be better, I'll update that

Comment on lines 15 to 18
/**
* An [okhttp3.EventListener] that delegates to a chain of EventListeners.
* Forward events are sent in order, reverse events are sent in reverse order.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I might say that "Start event are sent in forward order, terminal events are sent in reverse order."

Comment on lines 19 to 20
@InternalApi
public class EventListenerChain(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why does this need to be @InternalApi public?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be made internal

Comment on lines 109 to 116
override fun cacheConditionalHit(call: Call, cachedResponse: Response): Unit =
reverseListeners.forEach { it.cacheConditionalHit(call, cachedResponse) }

override fun cacheHit(call: Call, response: Response): Unit =
reverseListeners.forEach { it.cacheHit(call, response) }

override fun cacheMiss(call: Call): Unit =
reverseListeners.forEach { it.cacheMiss(call) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The cache events are non-terminal and should be handled front-to-back.

Comment on lines 88 to 96
/**
* 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,
connectionMonitoringListener: ConnectionMonitoringEventListener? = null,
): OkHttpClient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Do we still set a poolOverride anywhere? Can that parameter be removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't but our users might be? Since this is going in v1.5 it's probably fine to remove

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only method that allowed passing a pool override was previously private so users couldn't be using it. Let's remove the parameter if we don't need it anymore.

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@lauzadis lauzadis requested a review from ianbotsf July 3, 2025 18:13

/**
* An [okhttp3.EventListener] that delegates to a chain of EventListeners.
* Start event are sent in forward order, terminal events are sent in reverse order
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "Start event" -> "Start events"

@@ -102,7 +112,7 @@ private fun OkHttpEngineConfig.buildClientFromConfig(
writeTimeout(config.socketWriteTimeout.toJavaDuration())

// use our own pool configured with the timeout settings taken from config
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This comment isn't accurate anymore

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it is. We are creating our own pool using timeout settings taken from config

This comment has been minimized.

@lauzadis
Copy link
Contributor Author

lauzadis commented Jul 7, 2025

Downstream CI is failing until Kotlin 2.2.0 upgrade is merged in aws-sdk-kotlin

This comment has been minimized.

@lauzadis lauzadis merged commit cec880a into v1.5-main Jul 8, 2025
24 of 25 checks passed
@lauzadis lauzadis deleted the fix-connection-listener branch July 8, 2025 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
acknowledge-api-break Acknowledge that a change is API breaking and may be backwards-incompatible. Review carefully! acknowledge-artifact-size-increase
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants