Skip to content
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

fix: inconsistent custom okhttp configuration #901

Merged

Conversation

OmarAlJarrah
Copy link
Contributor

@OmarAlJarrah OmarAlJarrah commented Dec 16, 2024

Situation

In pull-request#857, we proposed a new feature that enables users to inject their own OkHttp client when configuring the SDK client.

A bug was reported to us that custom Dispatcher configurations on OkHttpClient instances are being overridden by OkHttp's default configurations. Look the example below:

val okhttp = OkHttpClient.Builder()
    .dispatcher(
        Dispatcher().apply {
            maxRequests = 5000
            maxRequestsPerHost = 1000
        }
    )
    .build()

val client = RapidClient.builderWithHttpClient()
    .okHttpClient(okHttpClient = okhttp)
    .key("key")
    .secret("secret")
    .build()

In the previous example, we expect maxRequests and maxRequestsPerHost values to be configured properly to their given values. Unfortunately, that's not the case, and configurations are overridden to OkHttp's default values.
image

We pass custom OkHttpClient instance to Ktor through the HttpClientEngineFactory utility, which enables us to create and configure instances of the OkHttpEngine.

abstract class BaseRapidClient(
    namespace: String,
    clientConfiguration: RapidClientConfiguration,
    httpClientEngine: HttpClientEngine = DEFAULT_HTTP_CLIENT_ENGINE,
) : Client(namespace) {
    // code... code... code... 
    private val engine: HttpClientEngine =
        _configurationProvider.okHttpClient?.let {
            OkHttp.create {
                preconfigured = it
            }
        } ?: httpClientEngine
    // code... code... code... 
}

When we pass an OkHttpClient instance to the Ktor client, Ktor attempts to clone our OkHttpClient instance, then executing our configuration code-block. The issue is that Ktor creates a new dispatcher instance and setting it on the new instance after cloning our client. Looking the Ktor code-base, we can observe that behavior in the following code:

public class OkHttpEngine(override val config: OkHttpConfig) : HttpClientEngineBase("ktor-okhttp") {
    // code... code... code... 
    private val clientCache = createLRUCache(::createOkHttpClient, {}, config.clientCacheSize)

    // code... code... code... 
    private fun createOkHttpClient(timeoutExtension: HttpTimeout.HttpTimeoutCapabilityConfiguration?): OkHttpClient {
        val builder = (config.preconfigured ?: okHttpClientPrototype).newBuilder()

        builder.dispatcher(Dispatcher())
        builder.apply(config.config)
        config.proxy?.let { builder.proxy(it) }
        timeoutExtension?.let {
            builder.setupTimeoutAttributes(it)
        }

        return builder.build()
    }
}

Breaking down the code, we can observe the following facts:

  1. Our OkHttpClient instance is cloned and transformed into an instance of OkHttpBuilder.
val builder = (config.preconfigured ?: okHttpClientPrototype).newBuilder()
  1. A new Dispatcher instance is created and set on the new builder.
builder.dispatcher(Dispatcher())
  1. Our custom-configuration block is executed on the OkHttpBuilder instance.
builder.apply(config.config)

As a result, our custom Dispatcher instance is overridden by a new instance holding the default values set in the Dispatcher class.

Task

In order to resolve the issue and persist custom Dispatcher instances, we need to make sure the the Dispatcher instances is set again properly in the ustom-configuration block. We do not need to care about other values and configurations since they are cloned properly and not overridden later on.

Action

For each client, we made sure Dispatcher instances are set properly on the custom-configuration block level.

OkHttp.create {
     config {
         preconfigured = it
         dispatcher(it.dispatcher)
     }
}

Testing

Verified manually. It would be hard to unit-test setting custom OkHttpClient instances because the instance is set to be inaccessible (private-visibility).

Results

Custom Dispatcher instances are now preserved and respected. Given the following configuration as a sample:

val okhttp = OkHttpClient.Builder()
    .dispatcher(
        Dispatcher().apply {
            maxRequests = 5000
            maxRequestsPerHost = 1000
        }
    )
    .build()

val client = RapidClient.builderWithHttpClient()
    .okHttpClient(okHttpClient = okhttp)
    .key("key")
    .secret("secret")
    .build()

We can observe that our configurations are respected and preserved for each request.
image

Notes

NaN

@OmarAlJarrah OmarAlJarrah requested a review from a team as a code owner December 16, 2024 12:46
Copy link
Member

@mohnoor94 mohnoor94 left a comment

Choose a reason for hiding this comment

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

You may test the behaviour by setting some config (e,g, timeout) on a custom client.

@OmarAlJarrah
Copy link
Contributor Author

You may test the behaviour by setting some config (e,g, timeout) on a custom client.

We have agreed to take this conversation into a new scope, potentially in another PR.

@mohnoor94 mohnoor94 dismissed their stale review December 17, 2024 10:57

Please create tickets for the things you agreed on.

@OmarAlJarrah OmarAlJarrah merged commit 26243b6 into main Dec 17, 2024
4 checks passed
@OmarAlJarrah OmarAlJarrah deleted the OmarAlJarrah/fix-inconsistent-custom-okhttp-configurations branch December 17, 2024 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants