Edoardo Luppi
03/26/2024, 1:34 PMdispatcher in SelectorManager(dispatcher = <http://Dispatchers.IO|Dispatchers.IO>)?
In my KMP library, I'm wrapping Ktor in a way similar to:
override suspend fun connect(address: NetAddress): ServerProperties {
selectorManager = SelectorManager(dispatcher)
val tcp = aSocket(selectorManager).tcp()
socket = tcp.connect(address.host, address.port)
...
I suppose a SelectorManager should be used to create multiple socket connections, so having one SelectorManager instance per socket instance isn't strictly correct.Oleg Yukhnevich
03/26/2024, 1:41 PMSelectorManager on JVM and Native behaves differently regarding dispatcher:
• on Native, dispatcher is ignored and separate Worker is created for handling of non-blocking sockets selection
• on JVM, it uses dispatcher and fully blocks (in a blocking loop) one thread from dispatcher (that's why you need IO and not Default)
And yes, one selector should be enough to handle multiple sockets (client/server)
You can check on how ktor CIO engine (client or server) is implemented to get more insightsEdoardo Luppi
03/26/2024, 1:49 PMSelectorManager.
When I instantiate the JVM socket client abstraction, e.g. MyJVMSocketClient(), I instantiate it using a custom coroutine scope, and a custom dispatcher that limits to a single thread. That is, each MyJVMSocketClient instance should use one thread at most.
public actual fun Dispatchers.forSocket(): CoroutineDispatcher =
IO.limitedParallelism(1)
However, currently, inside MyJVMSocketClient.connect I'm doing
override suspend fun connect(address: NetAddress): ServerProperties {
selectorManager = SelectorManager(dispatcher = <http://Dispatchers.IO|Dispatchers.IO>)
Which is obviously wrong from a logical perspective, because I'm not even re-using the forSocket() dispatcher.Edoardo Luppi
03/26/2024, 2:08 PMselectorManager = SelectorManager(currentCoroutineContext())
This would ensure the context is set outside of the socket client abstractionOleg Yukhnevich
03/26/2024, 2:42 PMSelectorManager instances, as it could suffer performance. As I said, on JVM it will block one thread - and using it for one socket will be very not efficient
I'm not sure which thread pool / dispatcher should be passed to theI would say, it's better to pass.SelectorManager
<http://Dispatchers.IO|Dispatchers.IO> like in your example, it should work fine, though, It still would be better to reuse it per sockets
Why do you need separate dispatcher for socket with io.limitedParallelism(1)?Edoardo Luppi
03/26/2024, 3:02 PMio.limitedParallelism(1)?
That's actually a good question, in the sense that I don't recall why it was done like that a long time ago.
It was probably done because of the misunderstanding between concurrency and parallelism, or probably because the idea was to constrain access to resources for each active socket connectionEdoardo Luppi
03/26/2024, 3:05 PMconstrain access to resourcesIn the sense we did not want to let coroutines use/spawn N threads freely, we wanted to use the bare minimum.
Oleg Yukhnevich
03/26/2024, 4:24 PMSelectorManager per all your client sockets, it should be closed to free resources after all client sockets are closed - or you will have a deadlock
2. use SelectorManager(IO) + handle sockets on IO until it becomes a problem
3. If for some reason you will see, that there are some problems with performance, I would suggest to create a separate dispatcher or via IO.limited(N) (though, I haven't tested it, but most likely it should work) or via newFixedThreadPoolContext (don't forget to close it) , where N > 1 (as 1 thread will be blocked) to reduce amount of threads used (and so memory and thread-switching)
4. Don't create a lot of SelectorManager instances, as it will cause a lot of platform threads to be just blocked, and so reduce performance (may be Virtual Threads will work fine here, but I haven't tested it)
5. it's fine to use different dispatcher for SelectorManager and sockets handling, but it will cause additional thread-switching, which could or couldn't affect your use case 🙂
6. Before replacing IO with something else - try to understand the problem better and if possible do some benchmarking/stress testing
If you have an open-source repository somewhere, it could help to understand better what do you want to achieve 🙂
Still, ktor-network is really not so easy to use, so don't be shy to ask questions here, I will try to help with all my knowledge received during development/research for rsocket-kotlin, or folks from ktor could also helpEdoardo Luppi
03/26/2024, 4:32 PMEdoardo Luppi
03/26/2024, 4:39 PMOleg Yukhnevich
03/26/2024, 4:43 PMSelectorManager is like a coroutines abstraction over it (on JVM, and similar concept on Native)
So, yes, basically it's a coordinator which decides when some of the application sockets should read/write to platform socketEdoardo Luppi
03/26/2024, 4:43 PMEdoardo Luppi
03/26/2024, 4:44 PMOleg Yukhnevich
03/26/2024, 4:46 PMselect (https://docs.oracle.com/javase/8/docs/api/java/nio/channels/Selector.html#select--)
This method performs a blocking selection operation. It returns only after at least one channel is selectedSo, if sockets are idle it just blocks thread awaiting for read/write
Edoardo Luppi
03/26/2024, 4:47 PMOleg Yukhnevich
03/26/2024, 4:49 PMktor-network as mostly internal ktor module to support CIO engine 🙂
At least, this is how I see it right now 🙃
I still don't fully understand how to use it efficiently and frequently looking at CIO (client/server) engines implementation to understand what's going onEdoardo Luppi
03/26/2024, 4:51 PMjava.net.Socket in the wrapper, instead of using KtorEdoardo Luppi
03/26/2024, 4:51 PMEdoardo Luppi
03/26/2024, 6:34 PM<http://Dispatchers.IO|Dispatchers.IO>. The only problem I had to workaround was closing it.
The way the library is structured prevents me from allowing consumers to explicitly close/dispose resources, so I had to register a shutdown hook.
I'm not sure which kind of resources the NIO selector allocates, and what would happen if I didn't use an hook, but just to be safe it looks like a decent workaround.Oleg Yukhnevich
03/26/2024, 7:16 PMEdoardo Luppi
03/27/2024, 3:34 PMlimitedParallelism
If I understand correctly the documentation, if for each socket connection I call limitedParallelism(1), and I have 1000 connections, the underlying pool will be expanded to up to 1000 threads?
If my understanding is correct, my usage of limitedParallelism is dangerousOleg Yukhnevich
03/27/2024, 4:16 PM<http://Dispatchers.IO|Dispatchers.IO> for all sockets will be fine (without limitedParallelism), it's capped to 64 threadsEdoardo Luppi
03/27/2024, 4:29 PM<http://Dispatchers.IO|Dispatchers.IO> - will allocate 1 up to 64 threads on demand, capped to 64 or number of cores
2. Use a custom ExecutorService - however this will require manual shutdown. Added complexity, worth it? Maybe not.
Since the executor is an implementation detail, I'd have to find a way to shut it down without the consumer knowing about it.Edoardo Luppi
03/27/2024, 4:34 PM