What's the expected lifecycle for a `SelectorManag...
# ktor
c
What's the expected lifecycle for a
SelectorManager
? Should I call
.close()
on it? Should I cancel its coroutine context? Do the sockets still work after the selector has been closed?
a
SelectorManager
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).
🙏 1
o
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).
AFAIK, it's not entirely correct, as
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.
c
When I call
.cancel()
on the job I passed to the
SelectorManager
, it remains
SUSPENDED
indefinitely instead of cancelling:
Copy code
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?
a
Can you please share a code snippet to reproduce the issue with cancellation?
c
Sorry, it took me a while to reduce it.
Copy code
val 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:
Copy code
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.
The stacktrace of the suspended cancelling selector coroutine:
Copy code
Coroutine "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())'
a
It seems the
SelectorManger
can be closed properly only when the
close
method is called. Your code works as expected with the following line:
Copy code
manager.await().close()
c
Issue created: KTOR-9683 Sockets: cancelling a SelectorManager job's doesn't close it properly
a
Thank you.