Open vasilmkd opened 3 years ago
And here's the same app with the work stealing runtime:
package cats.effect
package example
object Example extends IOApp.Simple {
def run: IO[Unit] =
IO.executionContext.flatMap { ec =>
IO {
for (_ <- 0 until 20) {
ec.execute(() => throw new RuntimeException("Boom!"))
}
}
} *> IO.never[Unit]
}
And the Mission Control screenshot:
I guess the question I have is whether it's Cats Effect's responsibility to protect the user from potentially ruining the compute thread pool with random Runnable
s which may throw?
It's hard to comment on the specifics of you idea on how to solve this problem, because I don't know how the idea. So I'll stay at an abstract level, and just say which of this aspects I see as important.
The WorkStealingThreadPool
is currently getting a large performance handicap for trying to be more protective then other ExecutionContext
s. I would love to see it show it's full performance potential.
I think the inconsistent behaviour on error is a big problem for someone swapping ExecutionContext
implementations. As far as I understand, we can't really bend the other ExecutionContext
implementations to behave like WorkStealingThreadPool
(we can wrap them in something but that doesn't really stop someone from submitting Runnable
s directly to it), but we can make WorkStealingThreadPool
behave like other execution contexts at least in the sense that throwing exception in WorkStealingThreadPool
would no longer shutdown the application.
Overall, my opinion is that it's cats-effect
's job to protect us when we use pure cats-effect
API. But when we use external, impure APIs like ExecutionContext
's API, it's NOT cats-effect
's job to protect us.
Nonetheless I would expect the WorkStealingThreadPool
to not loose threads like newFixedThreadPool
does ;).
@vasilmkd What's the current status on this?
I believe the discrepancy I described in the original post still exists between our own workstealing pool and a third party one, as no real work has been done. If desirable, the solution isn't too hard. We need to wrap runnables posted on the executor in our own runnable type which has a try catch block.
The work stealing runtime is safer when it comes to handling exceptions through running
Runnable
s throughEC#execute
than when using a differentExecutor
. Throwing exceptions on the worker threads of a fixed thread pool results in the death of those threads, and depending on the configuration of theThreadFactory
, those same threads may or may not be replaced by new ones. Furthermore, throwing fatal exceptions in aRunnable
on any other thread pool will not currently result in a graceful shutdown of anIOApp
, because we have bespoke logic to handle this as part of theIOFiber
runloop. Currently, this safety comes at a performance disadvantage for the work stealing runtime by having to suspend every submitted runnable in anIO.delay
. I think I have a way of equalizing the exception handling between the work stealing thread pool and any other thread pool used as a compute pool for IO and improving the performance of the work stealing runtime as a normal execution context for runningRunnable
s, but this essentially comes at a performance cost of otherExecutionContext
s used as a compute pool.Here is a sample application with a fixed thread pool that shows this behavior:
Executing this application results in the following situation in JDK Misssion Control where all
io-compute
threads are dead: