Skip to content

Commit a22852c

Browse files
authored
KTOR-6292 Make client use Dispatchers.IO by default (#3748)
* KTOR-6292 Make client use Dispatchers.IO by default
1 parent 12694bf commit a22852c

File tree

26 files changed

+52
-177
lines changed

26 files changed

+52
-177
lines changed

ktor-client/ktor-client-android/api/ktor-client-android.api

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ public final class io/ktor/client/engine/android/AndroidClientEngine : io/ktor/c
88
public fun execute (Lio/ktor/client/request/HttpRequestData;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
99
public synthetic fun getConfig ()Lio/ktor/client/engine/HttpClientEngineConfig;
1010
public fun getConfig ()Lio/ktor/client/engine/android/AndroidEngineConfig;
11-
public fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher;
1211
public fun getSupportedCapabilities ()Ljava/util/Set;
1312
}
1413

ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidClientEngine.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import io.ktor.client.engine.*
99
import io.ktor.client.plugins.*
1010
import io.ktor.client.plugins.sse.*
1111
import io.ktor.client.request.*
12-
import io.ktor.client.utils.*
1312
import io.ktor.http.*
1413
import io.ktor.http.content.*
1514
import io.ktor.util.*
@@ -31,13 +30,6 @@ private val METHODS_WITHOUT_BODY = listOf(HttpMethod.Get, HttpMethod.Head)
3130
@OptIn(InternalAPI::class)
3231
public class AndroidClientEngine(override val config: AndroidEngineConfig) : HttpClientEngineBase("ktor-android") {
3332

34-
override val dispatcher: CoroutineDispatcher by lazy {
35-
Dispatchers.clientDispatcher(
36-
config.threadsCount,
37-
"ktor-android-dispatcher"
38-
)
39-
}
40-
4133
override val supportedCapabilities: Set<HttpClientEngineCapability<*>> = setOf(HttpTimeout, SSECapability)
4234

4335
override suspend fun execute(data: HttpRequestData): HttpResponseData {

ktor-client/ktor-client-apache/jvm/src/io/ktor/client/engine/apache/ApacheEngine.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import io.ktor.client.engine.*
88
import io.ktor.client.plugins.*
99
import io.ktor.client.plugins.sse.*
1010
import io.ktor.client.request.*
11-
import io.ktor.client.utils.*
1211
import io.ktor.utils.io.*
1312
import kotlinx.coroutines.*
1413
import org.apache.http.*
@@ -22,13 +21,6 @@ private const val IO_THREAD_COUNT_DEFAULT = 4
2221
@OptIn(InternalAPI::class)
2322
internal class ApacheEngine(override val config: ApacheEngineConfig) : HttpClientEngineBase("ktor-apache") {
2423

25-
override val dispatcher by lazy {
26-
Dispatchers.clientDispatcher(
27-
config.threadsCount,
28-
"ktor-apache-dispatcher"
29-
)
30-
}
31-
3224
override val supportedCapabilities = setOf(HttpTimeout, SSECapability)
3325

3426
private val engine: CloseableHttpAsyncClient = prepareClient().apply { start() }

ktor-client/ktor-client-apache5/jvm/src/io/ktor/client/engine/apache5/Apache5Engine.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import io.ktor.client.engine.*
88
import io.ktor.client.plugins.*
99
import io.ktor.client.plugins.sse.*
1010
import io.ktor.client.request.*
11-
import io.ktor.client.utils.*
1211
import io.ktor.utils.io.*
1312
import kotlinx.coroutines.*
1413
import org.apache.hc.client5.http.config.*
@@ -30,13 +29,6 @@ private const val IO_THREAD_COUNT_DEFAULT = 4
3029
@OptIn(InternalAPI::class)
3130
internal class Apache5Engine(override val config: Apache5EngineConfig) : HttpClientEngineBase("ktor-apache") {
3231

33-
override val dispatcher by lazy {
34-
Dispatchers.clientDispatcher(
35-
config.threadsCount,
36-
"ktor-apache-dispatcher"
37-
)
38-
}
39-
4032
override val supportedCapabilities = setOf(HttpTimeout, SSECapability)
4133

4234
@Volatile

ktor-client/ktor-client-cio/jvmAndNix/src/io/ktor/client/engine/cio/CIOEngine.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import io.ktor.client.plugins.*
99
import io.ktor.client.plugins.sse.*
1010
import io.ktor.client.plugins.websocket.*
1111
import io.ktor.client.request.*
12-
import io.ktor.client.utils.*
1312
import io.ktor.http.*
1413
import io.ktor.network.selector.*
1514
import io.ktor.util.*
@@ -25,9 +24,6 @@ internal class CIOEngine(
2524
override val config: CIOEngineConfig
2625
) : HttpClientEngineBase("ktor-cio") {
2726

28-
override val dispatcher: CoroutineDispatcher =
29-
Dispatchers.clientDispatcher(config.threadsCount, "ktor-cio-dispatcher")
30-
3127
override val supportedCapabilities =
3228
setOf(HttpTimeout, WebSocketCapability, WebSocketExtensionsCapability, SSECapability)
3329

ktor-client/ktor-client-core/api/ktor-client-core.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ public abstract class io/ktor/client/engine/HttpClientEngineBase : io/ktor/clien
150150
public fun <init> (Ljava/lang/String;)V
151151
public fun close ()V
152152
public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext;
153+
public fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher;
153154
public fun getSupportedCapabilities ()Ljava/util/Set;
154155
public fun install (Lio/ktor/client/HttpClient;)V
155156
}
@@ -188,7 +189,6 @@ public abstract class io/ktor/client/engine/HttpClientJvmEngine : io/ktor/client
188189
public fun close ()V
189190
protected final fun createCallContext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
190191
public fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext;
191-
public fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher;
192192
public fun getSupportedCapabilities ()Ljava/util/Set;
193193
public fun install (Lio/ktor/client/HttpClient;)V
194194
}

ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineBase.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import kotlin.coroutines.*
1818
public abstract class HttpClientEngineBase(private val engineName: String) : HttpClientEngine {
1919
private val closed = atomic(false)
2020

21+
override val dispatcher: CoroutineDispatcher = ioDispatcher()
22+
2123
override val coroutineContext: CoroutineContext by lazy {
2224
SilentSupervisor() + dispatcher + CoroutineName("$engineName-context")
2325
}
@@ -28,9 +30,6 @@ public abstract class HttpClientEngineBase(private val engineName: String) : Htt
2830
val requestJob = coroutineContext[Job] as? CompletableJob ?: return
2931

3032
requestJob.complete()
31-
requestJob.invokeOnCompletion {
32-
dispatcher.close()
33-
}
3433
}
3534
}
3635

@@ -54,3 +53,5 @@ private fun CoroutineDispatcher.close() {
5453
// Some closeable dispatchers like Dispatchers.IO can't be closed.
5554
}
5655
}
56+
57+
internal expect fun ioDispatcher(): CoroutineDispatcher

ktor-client/ktor-client-core/common/src/io/ktor/client/engine/HttpClientEngineConfig.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public open class HttpClientEngineConfig {
1414
/**
1515
* Specifies network threads count advice.
1616
*/
17+
@Deprecated(
18+
"The [threadsCount] property is deprecated. The [Dispatchers.IO] is used by default.",
19+
level = DeprecationLevel.ERROR
20+
)
1721
public var threadsCount: Int = 4
1822

1923
/**
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package io.ktor.client.engine
6+
7+
import kotlinx.coroutines.*
8+
9+
internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.Default

ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/JsClientEngine.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ internal class JsClientEngine(
2525
override val config: HttpClientEngineConfig
2626
) : HttpClientEngineBase("ktor-js") {
2727

28-
override val dispatcher = Dispatchers.Default
29-
3028
override val supportedCapabilities = setOf(HttpTimeout, WebSocketCapability, SSECapability)
3129

3230
init {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package io.ktor.client.engine
6+
7+
import kotlinx.coroutines.*
8+
9+
internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.IO

ktor-client/ktor-client-core/jvm/src/io/ktor/client/engine/HttpClientJvmEngine.kt

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package io.ktor.client.engine
66

77
import io.ktor.util.*
88
import kotlinx.coroutines.*
9-
import java.util.concurrent.*
109
import kotlin.coroutines.*
1110

1211
/**
@@ -20,21 +19,9 @@ import kotlin.coroutines.*
2019
)
2120
public abstract class HttpClientJvmEngine(engineName: String) : HttpClientEngine {
2221
private val clientContext = SilentSupervisor()
23-
private val _dispatcher by lazy {
24-
Executors.newFixedThreadPool(config.threadsCount) {
25-
Thread(it).apply {
26-
isDaemon = true
27-
}
28-
}.asCoroutineDispatcher()
29-
}
30-
31-
@OptIn(InternalCoroutinesApi::class)
32-
override val dispatcher: CoroutineDispatcher
33-
get() = _dispatcher
3422

35-
@OptIn(InternalCoroutinesApi::class)
3623
override val coroutineContext: CoroutineContext by lazy {
37-
_dispatcher + clientContext + CoroutineName("$engineName-context")
24+
dispatcher + clientContext + CoroutineName("$engineName-context")
3825
}
3926

4027
/**
@@ -60,12 +47,8 @@ public abstract class HttpClientJvmEngine(engineName: String) : HttpClientEngine
6047
}
6148

6249
override fun close() {
63-
val job = clientContext[Job] as CompletableJob
64-
50+
val job = clientContext.job as CompletableJob
6551
job.complete()
66-
job.invokeOnCompletion {
67-
_dispatcher.close()
68-
}
6952
}
7053
}
7154

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright 2014-2023 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package io.ktor.client.engine
6+
7+
import kotlinx.coroutines.*
8+
9+
internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.IO

ktor-client/ktor-client-java/api/ktor-client-java.api

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ public final class io/ktor/client/engine/java/JavaHttpEngine : io/ktor/client/en
1515
public fun execute (Lio/ktor/client/request/HttpRequestData;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
1616
public synthetic fun getConfig ()Lio/ktor/client/engine/HttpClientEngineConfig;
1717
public fun getConfig ()Lio/ktor/client/engine/java/JavaHttpConfig;
18-
public fun getDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher;
1918
public fun getSupportedCapabilities ()Ljava/util/Set;
2019
}
2120

ktor-client/ktor-client-java/jvm/src/io/ktor/client/engine/java/JavaHttpEngine.kt

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,16 @@ import io.ktor.client.plugins.sse.*
1010
import io.ktor.client.plugins.websocket.*
1111
import io.ktor.client.request.*
1212
import io.ktor.utils.io.*
13-
import kotlinx.atomicfu.*
1413
import kotlinx.coroutines.*
1514
import java.net.*
1615
import java.net.http.*
1716
import java.time.*
1817
import java.time.temporal.*
19-
import java.util.concurrent.*
2018

2119
public class JavaHttpEngine(override val config: JavaHttpConfig) : HttpClientEngineBase("ktor-java") {
2220

23-
private val executorThreadCounter = atomic(0L)
24-
2521
private val protocolVersion = config.protocolVersion
2622

27-
/**
28-
* Exposed for tests only.
29-
*/
30-
internal val executor by lazy {
31-
Executors.newFixedThreadPool(config.threadsCount) {
32-
val number = executorThreadCounter.getAndIncrement()
33-
Thread(it, "ktor-client-java-$number").apply {
34-
isDaemon = true
35-
setUncaughtExceptionHandler { _, _ -> }
36-
}
37-
}
38-
}
39-
40-
public override val dispatcher: CoroutineDispatcher by lazy {
41-
executor.asCoroutineDispatcher()
42-
}
43-
4423
public override val supportedCapabilities: Set<HttpClientEngineCapability<*>> =
4524
setOf(HttpTimeout, WebSocketCapability, SSECapability)
4625

@@ -75,7 +54,7 @@ public class JavaHttpEngine(override val config: JavaHttpConfig) : HttpClientEng
7554
return httpClient ?: synchronized(this) {
7655
httpClient ?: HttpClient.newBuilder().apply {
7756
version(protocolVersion)
78-
executor(executor)
57+
executor(dispatcher.asExecutor())
7958

8059
apply(config.config)
8160

@@ -106,6 +85,7 @@ public class JavaHttpEngine(override val config: JavaHttpConfig) : HttpClientEng
10685

10786
proxy(ProxySelector.of(address))
10887
}
88+
10989
Proxy.Type.DIRECT -> proxy(HttpClient.Builder.NO_PROXY)
11090
else -> error("Java HTTP engine does not currently support $type proxies.")
11191
}

ktor-client/ktor-client-java/jvm/test/io/ktor/client/engine/java/JavaEngineTests.kt

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,9 @@ import io.ktor.http.*
1313
import kotlinx.coroutines.*
1414
import java.net.*
1515
import java.time.*
16-
import java.util.concurrent.*
1716
import kotlin.test.*
1817

1918
class JavaEngineTests {
20-
@Test
21-
fun testClose() {
22-
val engine = JavaHttpEngine(JavaHttpConfig())
23-
engine.close()
24-
25-
assertTrue("Java HTTP dispatcher is not working.") {
26-
engine.executor.isShutdown
27-
}
28-
}
2919

3020
@Test
3121
fun testProxy() = runBlocking {
@@ -41,49 +31,6 @@ class JavaEngineTests {
4131
assertEquals("proxy", body)
4232
}
4333

44-
@Test
45-
fun testThreadLeak() = runBlocking {
46-
System.setProperty("jdk.internal.httpclient.selectorTimeout", "50")
47-
48-
val initialNumberOfThreads = Thread.getAllStackTraces().size
49-
val repeats = 25
50-
val executors = ArrayList<ExecutorService>()
51-
52-
try {
53-
repeat(repeats) {
54-
HttpClient(Java).use { client ->
55-
val response = client.get("http://www.google.com").body<String>()
56-
assertNotNull(response)
57-
executors += (client.engine as JavaHttpEngine).executor
58-
}
59-
}
60-
61-
// When engine is disposed HttpClient's SelectorManager thread remains active
62-
// until it realizes that no more reference on HttpClient.
63-
// Minimum polling interval SelectorManager thread is 1000ms.
64-
var retry = 0
65-
do {
66-
System.gc()
67-
Thread.sleep(1000)
68-
System.gc()
69-
System.gc()
70-
} while (Thread.getAllStackTraces().size >= initialNumberOfThreads && retry++ < 10)
71-
} finally {
72-
System.clearProperty("jdk.internal.httpclient.selectorTimeout")
73-
}
74-
75-
val totalNumberOfThreads = Thread.getAllStackTraces().size
76-
val threadsCreated = totalNumberOfThreads - initialNumberOfThreads
77-
78-
executors.forEach { pool ->
79-
assertTrue(pool.isTerminated)
80-
}
81-
82-
assertTrue("Number of threads should be less $repeats, but was $threadsCreated") {
83-
threadsCreated < repeats
84-
}
85-
}
86-
8734
@Test
8835
fun testRequestAfterRecreate() {
8936
runBlocking {

ktor-client/ktor-client-jetty-jakarta/jvm/src/io/ktor/client/engine/jetty/jakarta/JettyHttp2Engine.kt

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,16 @@ package io.ktor.client.engine.jetty.jakarta
77
import io.ktor.client.engine.*
88
import io.ktor.client.plugins.*
99
import io.ktor.client.request.*
10-
import io.ktor.client.utils.*
1110
import io.ktor.util.*
1211
import io.ktor.utils.io.*
1312
import kotlinx.coroutines.*
1413
import org.eclipse.jetty.http2.client.*
15-
import org.eclipse.jetty.util.thread.*
1614

1715
@OptIn(InternalAPI::class)
1816
internal class JettyHttp2Engine(
1917
override val config: JettyEngineConfig
2018
) : HttpClientEngineBase("ktor-jetty") {
2119

22-
override val dispatcher: CoroutineDispatcher by lazy {
23-
Dispatchers.clientDispatcher(
24-
config.threadsCount,
25-
"ktor-jetty-dispatcher"
26-
)
27-
}
28-
2920
override val supportedCapabilities = setOf(HttpTimeout)
3021

3122
/**
@@ -59,9 +50,7 @@ internal class JettyHttp2Engine(
5950
addBean(config.sslContextFactory)
6051
check(config.proxy == null) { "Proxy unsupported in Jetty engine." }
6152

62-
executor = QueuedThreadPool().apply {
63-
name = "ktor-jetty-client-qtp"
64-
}
53+
executor = Dispatchers.IO.asExecutor()
6554

6655
setupTimeoutAttributes(timeoutExtension)
6756

0 commit comments

Comments
 (0)