We need to add an IORuntimeConfig parameter for a PollingSystem:
abstract class PollingSystem {
protected[unsafe] def init(): PollingState
protected[unsafe] def poll(state: PollingState, timeoutNanos: Long): Boolean
protected[unsafe] def unpark(thread: Thread): Unit
protected[unsafe] def close(state: PollingState): Unit
}
// pure marker trait
trait PollingStateObviously, it should be possible to define this value both as a protected override within IOApp as well as the IORuntimeConfig itself. The former allows some degree of compositionality if the downstream implementor wants to define a custom IOApp subtype.
The contract of PollingSystem is such that it must support at least interruptibly sleeping the thread for up to a timeout. Additional functionality may be supported and would be PollingSystem-specific.
Additionally, on IORuntime itself, we define the following:
def unsafeCurrentPollingState(): PollingState =
Thread.currentThread() match {
case wt: WorkerThread => wt.currentPollingState()
case _ => null
}This gives us an efficient thread local mechanism for the polling system, which in turn allows a downstream framework to get their PollingState instance which can be used to register callbacks. Note that this is a very unsafe, very untyped API in which a lot of casting will take place under common use.
The simplest PollingSystem is just what is necessary to handle integrated timers:
object SleepingPollingSystem extends PollingSystem {
def init(): PollingState = new PollingState {}
def poll(state: PollingState, timeoutNanos: Long): Boolean = {
Unsafe.parkNanos(timeoutNanos)
true
}
def unpark(thread: Thread): Unit =
LockSupport.unpark(thread)
def close(state: PollingState): Unit = ()
}In the event that we wanted to have a polling system which supports something like epoll, it would additionally need functionality to maintain the epoll file descriptor within its own PollingState (to which it would need to cast), as well as the ability to register for events. This event registration logic would be called by the downstream implementor itself, empowered via the IORuntime.unsafeCurrentPollingState() mechanism.
Personally I think this should be passed via
IORuntimerather thanIORuntimeConfig, as a pluggable component like the compute/blocking polls or the scheduler. The idea being that users are far more likely to want to use a customIORuntimeConfigthan a customIORuntime(indeed,IOAppdiscourages you from overridingIORuntime).In fact, if we go further it really feels like it should be passed as an argument here. Since after all this only makes sense with the work-stealing thread pool.
https://github.com/typelevel/cats-effect/blob/6cf9bdc6a5f7dd75999e12d3723b17dbca7824f7/core/jvm/src/main/scala/cats/effect/unsafe/IORuntimeCompanionPlatform.scala#L37-L43
This is also consistent with my current usage in epollcat in friends: I offer an
EpollRuntime(...)which takes anIORuntimeConfig(and eventually anEpollRuntimeConfig) and returns anIORuntime.