Skip to content

Commit efa9b3b

Browse files
authored
Don't re-use OkHttpClient outside Kotlin notebooks (#469)
Addresses #451 by isolating the OkHttpClient-sharing behavior to Kolin notebooks. Tests are added to prevent regressions for both notebooks (should share client resources by default) and elsewhere (should not share client resources by default). # #451 copy Raised by @mcumings. First worked around in [eBay/metrics-for-develocity-plugin code](https://github.com/eBay/metrics-for-develocity-plugin/blob/6748cd762ff8c1d6bf7b1f2e4001a46f956f940e/src/main/kotlin/com/ebay/plugins/metrics/develocity/service/DevelocityBuildService.kt#L50). The workaround was since replicated in one of the library examples, `example-gradle-task`: [`DevelocityApiService`](https://github.com/gabrielfeo/develocity-api-kotlin/blob/main/examples/example-gradle-task/buildSrc/src/main/kotlin/build/logic/DevelocityApiService.kt#L19-L24). OkHttpClient internal resources (via `OkHttpClient.newBuilder`) are re-used by default to fit the Kotlin notebook use case, in which the notebook cell that creates an API instance (`DevelocityApi.newInstance`) may be ran multiple times, with or without changes, in the same Kotlin kernel. If OkHttpClient resources were not re-used, this common scenario would result in high resource usage due to several sets of connection pools and executors being kept. At least in the case of variable renames (e.g. one cell executes with `val api = [...]` then value renamed to `val develocityApi = [...]`), the old `api` variable would not get GC'ed, since the Kotlin kernel holds on to past cell execution variables. Once client resources are shut down or `DevelocityApi.shutdown` is called, however, the client can no longer be used. In the case of usage in a Gradle plugin, this leads to the following: ```kotlin abstract class DevelocityApiService : DevelocityApi by DevelocityApi.newInstance(), BuildService<BuildServiceParameters.None>, AutoCloseable { override fun close() { shutdown() } } ``` 0. Build starts 1. Gradle constructs the `BuildService`. `DevelocityApi.newInstance` without a specified `clientBuilder` will create a new builder out of a [static and global client instance](https://github.com/gabrielfeo/develocity-api-kotlin/blob/cbc135a9c0d125dbeb4abbbd6ed8b9c9ff4360e3/library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/OkHttpClient.kt#L21) that is meant to support the re-using in Kotlin notebooks 3. Build ends 4. Gradle calls `close`, leading to the static and global client's resources to be closed Any new builds with that Gradle daemon will lead to the service being re-created without the "static and global client instance" being re-initialized. Since a client cannot be used after its resources (including the executor are closed). An "executor rejected" error is thrown by OkHttp. Can be reproduced by checking out parent of this [fix commit](a704a53). ### Workarounds: - Define your own `clientBuilder` as in the fixed version of [`DevelocityApiService`](https://github.com/gabrielfeo/develocity-api-kotlin/blob/main/examples/example-gradle-task/buildSrc/src/main/kotlin/build/logic/DevelocityApiService.kt#L19-L24) - [Untested] Don't shutdown the client (thread pool and connections would live on in the GradleDaemon process to be re-used by the next build, which avoids the problem and makes subsequent API requests faster at the cost of slightly increased memory usage by the idle daemon)
1 parent cbc135a commit efa9b3b

File tree

8 files changed

+91
-22
lines changed

8 files changed

+91
-22
lines changed

examples/example-gradle-task/buildSrc/src/main/kotlin/build/logic/DevelocityApiService.kt

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,11 @@ import org.gradle.api.services.BuildServiceParameters
77
import okhttp3.OkHttpClient
88

99
abstract class DevelocityApiService
10-
: DevelocityApi by DevelocityApi.newInstance(config()),
10+
: DevelocityApi by DevelocityApi.newInstance(),
1111
BuildService<BuildServiceParameters.None>,
1212
AutoCloseable {
1313

1414
override fun close() {
1515
shutdown()
1616
}
1717
}
18-
19-
private fun config() = Config(
20-
// Necessary to accomodate Gradle's build service lifecycle because library
21-
// uses a singleton OkHttpClient.Builder unless one is provided.
22-
// See https://github.com/gabrielfeo/develocity-api-kotlin/issues/451
23-
clientBuilder = OkHttpClient.Builder(),
24-
)

library/api/library.api

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,26 @@ public final class com/gabrielfeo/develocity/api/extension/BuildsApiExtensionsKt
203203
public static synthetic fun getGradleAttributesFlow$default (Lcom/gabrielfeo/develocity/api/BuildsApi;JLjava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lkotlinx/coroutines/CoroutineScope;Ljava/util/List;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
204204
}
205205

206+
public final class com/gabrielfeo/develocity/api/internal/FreshOkHttpClientBuilderFactory : com/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory {
207+
public fun <init> ()V
208+
public fun create ()Lokhttp3/OkHttpClient$Builder;
209+
}
210+
211+
public abstract interface class com/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory {
212+
public static final field Companion Lcom/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory$Companion;
213+
public abstract fun create ()Lokhttp3/OkHttpClient$Builder;
214+
}
215+
216+
public final class com/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory$Companion {
217+
public final fun getDefault ()Lcom/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory;
218+
public final fun setDefault (Lcom/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory;)V
219+
}
220+
221+
public final class com/gabrielfeo/develocity/api/internal/SharedOkHttpClientBuilderFactory : com/gabrielfeo/develocity/api/internal/OkHttpClientBuilderFactory {
222+
public fun <init> ()V
223+
public fun create ()Lokhttp3/OkHttpClient$Builder;
224+
}
225+
206226
public final class com/gabrielfeo/develocity/api/internal/auth/HttpBearerAuth : okhttp3/Interceptor {
207227
public fun <init> ()V
208228
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V

library/src/integrationTest/kotlin/com/gabrielfeo/develocity/api/internal/jupyter/DevelocityApiJupyterIntegrationTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import org.jetbrains.kotlinx.jupyter.api.Code
77
import org.jetbrains.kotlinx.jupyter.testkit.JupyterReplTestCase
88
import kotlin.reflect.KVisibility
99
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
import kotlin.test.assertNotEquals
1012

1113
@ExperimentalStdlibApi
1214
class DevelocityApiJupyterIntegrationTest : JupyterReplTestCase() {
@@ -21,6 +23,24 @@ class DevelocityApiJupyterIntegrationTest : JupyterReplTestCase() {
2123
attrs["custom value name"]
2224
""")
2325

26+
@Test
27+
fun `Given default clientBuilder, re-uses OkHttpClient resources`() {
28+
execSuccess("val api = DevelocityApi.newInstance()")
29+
execSuccess("val api2 = DevelocityApi.newInstance()")
30+
val connectionPool1 = execRendered("api.config.clientBuilder.build().connectionPool.hashCode()")
31+
val connectionPool2 = execRendered("api2.config.clientBuilder.build().connectionPool.hashCode()")
32+
assertEquals(connectionPool1, connectionPool2)
33+
}
34+
35+
@Test
36+
fun `Given custom clientBuilder set, does not re-use OkHttpClient resources`() {
37+
execSuccess("val api = DevelocityApi.newInstance(Config(clientBuilder = okhttp3.OkHttpClient.Builder()))")
38+
execSuccess("val api2 = DevelocityApi.newInstance(Config(clientBuilder = okhttp3.OkHttpClient.Builder()))")
39+
val connectionPool1 = execRendered("api.config.clientBuilder.build().connectionPool.hashCode()")
40+
val connectionPool2 = execRendered("api2.config.clientBuilder.build().connectionPool.hashCode()")
41+
assertNotEquals(connectionPool1, connectionPool2)
42+
}
43+
2444
@Test
2545
fun `imports all public classes`() {
2646
val classes = allPublicClassesRecursive("com.gabrielfeo.develocity.api")

library/src/main/kotlin/com/gabrielfeo/develocity/api/Config.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ data class Config(
8888
* The default is to share resources only within the library, i.e. multiple `Config()` with
8989
* the default [clientBuilder] will already share resources.
9090
*/
91-
val clientBuilder: OkHttpClient.Builder = basicOkHttpClient.newBuilder(),
91+
val clientBuilder: OkHttpClient.Builder = OkHttpClientBuilderFactory.default.create(),
9292

9393
/**
9494
* Maximum amount of concurrent requests allowed. Further requests will be queued. By default,

library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/OkHttpClient.kt

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,17 @@
11
package com.gabrielfeo.develocity.api.internal
22

3-
import com.gabrielfeo.develocity.api.Config
4-
import com.gabrielfeo.develocity.api.internal.auth.HttpBearerAuth
5-
import com.gabrielfeo.develocity.api.internal.caching.CacheEnforcingInterceptor
6-
import com.gabrielfeo.develocity.api.internal.caching.CacheHitLoggingInterceptor
3+
import com.gabrielfeo.develocity.api.*
4+
import com.gabrielfeo.develocity.api.internal.auth.*
5+
import com.gabrielfeo.develocity.api.internal.caching.*
76
import okhttp3.Cache
8-
import okhttp3.Interceptor
97
import okhttp3.OkHttpClient
108
import okhttp3.logging.HttpLoggingInterceptor
119
import okhttp3.logging.HttpLoggingInterceptor.Level.BASIC
1210
import okhttp3.logging.HttpLoggingInterceptor.Level.BODY
1311
import java.time.Duration
14-
import org.slf4j.Logger
1512

1613
private const val HTTP_LOGGER_NAME = "com.gabrielfeo.develocity.api.OkHttpClient"
1714

18-
/**
19-
* Base instance just so that multiple created [Config]s will share resources by default.
20-
*/
21-
internal val basicOkHttpClient by lazy {
22-
OkHttpClient.Builder().build()
23-
}
24-
2515
/**
2616
* Builds the final `OkHttpClient` with a `Config`.
2717
*/
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.gabrielfeo.develocity.api.internal
2+
3+
import okhttp3.OkHttpClient
4+
5+
interface OkHttpClientBuilderFactory {
6+
7+
companion object {
8+
var default: OkHttpClientBuilderFactory = FreshOkHttpClientBuilderFactory()
9+
}
10+
11+
fun create(): OkHttpClient.Builder
12+
}
13+
14+
/**
15+
* Creates a new [OkHttpClient.Builder] instance for every call to [create].
16+
*/
17+
class FreshOkHttpClientBuilderFactory : OkHttpClientBuilderFactory {
18+
override fun create() = OkHttpClient.Builder()
19+
}
20+
21+
/**
22+
* Re-uses the same [OkHttpClient.Builder] instance for every call to [create], allowing for
23+
* internal resources such as connection pools and threads to be shared between clients.
24+
*/
25+
class SharedOkHttpClientBuilderFactory : OkHttpClientBuilderFactory {
26+
private val sharedClient: OkHttpClient by lazy { OkHttpClient.Builder().build() }
27+
override fun create() = sharedClient.newBuilder()
28+
}

library/src/main/kotlin/com/gabrielfeo/develocity/api/internal/jupyter/DevelocityApiJupyterIntegration.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.gabrielfeo.develocity.api.internal.jupyter
22

3+
import org.jetbrains.kotlinx.jupyter.api.ExecutionCallback
34
import org.jetbrains.kotlinx.jupyter.api.KotlinKernelVersion
45
import org.jetbrains.kotlinx.jupyter.api.libraries.LibraryDefinition
56

@@ -13,4 +14,13 @@ class DevelocityApiJupyterIntegration : LibraryDefinition {
1314
"com.gabrielfeo.develocity.api.model.*",
1415
"com.gabrielfeo.develocity.api.extension.*",
1516
)
17+
18+
override val init: List<ExecutionCallback<*>> = listOf(
19+
{
20+
execute("""
21+
com.gabrielfeo.develocity.api.internal.OkHttpClientBuilderFactory.default =
22+
com.gabrielfeo.develocity.api.internal.SharedOkHttpClientBuilderFactory()
23+
""".trimIndent())
24+
}
25+
)
1626
}

library/src/test/kotlin/com/gabrielfeo/develocity/api/OkHttpClientTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ class OkHttpClientTest {
8080
}
8181
}
8282

83+
/**
84+
* Tests against regressions of issue gabrielfeo/develocity-api-kotlin#451
85+
*/
86+
@Test
87+
fun `Given no clientBuilder specified, OkHttpClient resources not re-used`() {
88+
assertNotEquals(buildClient().connectionPool, buildClient().connectionPool)
89+
}
90+
8391
private fun buildClient(
8492
vararg envVars: Pair<String, String?>,
8593
clientBuilder: OkHttpClient.Builder? = null,

0 commit comments

Comments
 (0)