CLOVIS
06/23/2026, 12:26 PMSelectorManager ?
Should I call .close() on it? Should I cancel its coroutine context? Do the sockets still work after the selector has been closed?Aleksei Tirman [JB]
06/23/2026, 12:49 PMSelectorManager is a single background worker that watches many sockets at once and wakes up each socket's coroutine when that socket is ready to read, write, connect, or accept. It should be alive for as long as you need to communicate through those sockets. You can call close() for a graceful shutdown that stops the background loop and fails any pending reads/writes, so nothing hangs. Canceling the manager's coroutine context also tears it down, but through the "error" path (it throws a CancellationException internally to break out of the loop).Oleg Yukhnevich
06/23/2026, 1:11 PMCanceling the manager's coroutine context also tears it down, but through the "error" path (it throws aAFAIK, it's not entirely correct, asinternally to break out of the loop).CancellationException
SelectorManager doesn't create a new Job under the hood. So if you create it with just SelectorManager(<http://Dispatchers.IO|Dispatchers.IO>), as I've seen in most usages in the wild, you will have nothing to cancel.
The only really correct and predictable way to release underlying resources is to use close after all other sockets created from this selector are closed (their socketContext is completed).
For sockets, in my experience, it's also better to use close and not rely entirely on the coroutine context.CLOVIS
06/23/2026, 1:19 PM.cancel() on the job I passed to the SelectorManager , it remains SUSPENDED indefinitely instead of cancelling:
Coroutine "selector#4":StandaloneCoroutine{Cancelling}@689a56ab, state: SUSPENDED
at io.ktor.network.selector.ActorSelectorManager.receiveOrNullSuspend(ActorSelectorManager.kt:168)
at io.ktor.network.selector.ActorSelectorManager.process(ActorSelectorManager.kt:90)
at io.ktor.network.selector.ActorSelectorManager$1.invokeSuspend(ActorSelectorManager.kt:44)
Shouldn't it respect the prompt cancellation guarantee most of the Coroutines library follows?Aleksei Tirman [JB]
07/01/2026, 8:16 AMCLOVIS
07/02/2026, 8:59 AMval address = coroutineScope {
val manager = CompletableDeferred<SelectorManager>()
val managerJob = launch(Dispatchers.Default + CoroutineName("FindMongoAddressManager")) {
SelectorManager(currentCoroutineContext())
.also { manager.complete(it) }
}
val hostName = "localhost"
println(" Searching for the address of the running MongoDB instance…")
val address = InetSocketAddress(hostName, 27017)
try {
println("» Trying $address…")
aSocket(manager.await()).tcp().connect(address.hostname, address.port) {
socketTimeout = 100
}.use {
println(" Connected successfully!")
}
manager.await().cancel("We found a valid address.")
managerJob.cancel("We found a valid address.")
println(" Closed the socket.")
return@coroutineScope address
} catch (e: Exception) {
println(" Could not connect to MongoDB on socket $address • $e")
}
error("Could not find on which port MongoDB is running.")
// No need to cancel the manager, it will be killed by the exception
}
println("Found address: $address")
You will need something (whatever) listening on localhost:27017 to run it.
This code dumps after 1 minute:
Coroutine "coroutine#76":BlockingCoroutine{Active}@3390c044, state: SUSPENDED
Coroutine TestScope[test started], state: RUNNING
Coroutine "kotlinx.coroutines.test runner#77":StandaloneCoroutine{Active}@4368e748, state: SUSPENDED
Coroutine "selector#79":StandaloneCoroutine{Cancelling}@367b4e05, state: SUSPENDED
… created by 'SelectorManager(currentCoroutineContext())'
I don't understand why it's marked as Cancelling but also SUSPENDED . Shouldn't prompt-cancellation guarantee mean it should switch to Cancelled immediately?
If I replace the manager.await().cancel("We found a valid address.") line by manager.await().close() (proper closing), then the test is successful in ~15ms, which is what I expected in all cases.
If I comment out the manager.await().cancel("We found a valid address.") line I get the same result as the first example, with four coroutines left hanging, so at least the structured concurrency seems to work.CLOVIS
07/02/2026, 9:02 AMCoroutine "selector#79":StandaloneCoroutine{Cancelling}@5312f891, state: SUSPENDED
at io.ktor.network.selector.ActorSelectorManager.receiveOrNullSuspend(ActorSelectorManager.kt:168)
at io.ktor.network.selector.ActorSelectorManager.process(ActorSelectorManager.kt:90)
at io.ktor.network.selector.ActorSelectorManager$1.invokeSuspend(ActorSelectorManager.kt:44)
at _COROUTINE._CREATION._(CoroutineDebugging.kt:30)
at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:161)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:44)
at kotlinx.coroutines.BuildersKt.launch$default(Unknown Source)
at io.ktor.network.selector.ActorSelectorManager.<init>(ActorSelectorManager.kt:39)
at io.ktor.network.selector.SelectorManagerKt.SelectorManager(SelectorManager.kt:13)
at …the line in the example with 'SelectorManager(currentCoroutineContext())'Aleksei Tirman [JB]
07/02/2026, 9:35 AMSelectorManger can be closed properly only when the close method is called. Your code works as expected with the following line:
manager.await().close()CLOVIS
07/02/2026, 9:45 AMAleksei Tirman [JB]
07/02/2026, 12:27 PM