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 PollingState
Obviously, 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.
This is reasonable. It also synergizes well with the type member suggestion. I tend to mentally blur the line between the config and the runtime itself, but I think you're right that this fits a bit better in the strata of the latter.
I didn't think of doing the member type. Agreed this is much nicer and it avoids the need to cast in user code if we do it right (there will be ugly casts in
IORuntime
itself).It probably does. Let's just assume it does. :-) I'll amend the API.
It can't just fail the associated callback?
Well, callbacks will be of type
Either[Throwable, A] => Unit
, so it can just run them directly. One of the things that isn't depicted here is the fact that subtypes will need to define their own mechanism for registering for events. I'm envisioning that this method will be on their subtype ofPollingSystem
and will take a callback as a parameter, which in turn will originally come from anIO.async
. ThePollingSystem
subtype will be responsible for hanging onto this callback value somewhere in itsPollingState
(on the JVM heap). This should be a relatively simple state management problem since implementors can assume single-writer on all things.