Cats-effect: IORunLoop can throw exceptions via IOShift that aren't caught in `IO`

Created on 28 May 2020  Â·  3Comments  Â·  Source: typelevel/cats-effect

I have a case where (I believe) my app code is leaking a Blocker outside of a Resource and trying to use after close.

This is resulting in my process dying, with the resulting error:

java.util.concurrent.RejectedExecutionException: Task cats.effect.internals.IOShift$Tick@70fe2b01 rejected from java.util.concurrent.ThreadPoolExecutor@83c9e2b[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 1]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at java.util.concurrent.Executors$DelegatedExecutorService.execute(Executors.java:668)
    at scala.concurrent.impl.ExecutionContextImpl.execute(ExecutionContextImpl.scala:24)
    at cats.effect.internals.IOShift$$anon$1.apply(IOShift.scala:29)
    at cats.effect.internals.IOShift$$anon$1.apply(IOShift.scala:27)
    at cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:352)
    at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:124)
    at cats.effect.internals.IORunLoop$.start(IORunLoop.scala:34)
    at cats.effect.internals.IOBracket$.$anonfun$apply$1(IOBracket.scala:43)
    at cats.effect.internals.IOBracket$.$anonfun$apply$1$adapted(IOBracket.scala:33)
    at cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:352)
    at cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:124)
    at cats.effect.internals.IORunLoop$RestartCallback.signal(IORunLoop.scala:366)
    at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:387)
    at cats.effect.internals.IORunLoop$RestartCallback.apply(IORunLoop.scala:330)
    at cats.effect.internals.IOShift$Tick.run(IOShift.scala:36)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

Given that RejectedExecutionException is a NonFatal exception, I would instead expect this to be translated to an IO failure, or at the very least that it gets wrapped with some kind of extra debugging information - it's taken a few days to be able to track this down and reproduce it.

Most helpful comment

It's a shame the types don't prevent you from accidentally using a resource outside of its use. I'm not sure whether we could achieve that without the luxury of fancy session/linear/dependent types.

It's definitely impossible (it follows relatively directly from the proof that linear logic cannot be embedded within predicate calculus). :-( Intuitively, the problem here is the fact that use has type (A => F[B]) => F[B]. Note that the B can be absolutely anything. This in turn means that you're always free to engineer a leak by composing use and pure.

If we were able to restrict B to be something less, then we could get provable resource safety (i.e. encoded in the types), but it would come at the cost of our domain. Oleg's original "monadic regions" paper actually does this. They devised a construct which behaves a bit like Resource in some respects (it's a dynamic region which automatically distributes over flatMap), and they do get provable safety out of it, but they were only able to do this by restricting the value domain to just the concrete algebra they had vetted.

Conceptually, we want to be able to express something like:

Resource[F, A] => (A => F[B]) => F[B] where B doesn't contain anything from any of the Resource values which were constructed via make.

Of course we… can't say that. At least not without making B be something very concrete, which is what Oleg did.

So it's a tradeoff. It's worth noting that bracket has this same problem. In a lot of ways, the best we can do here is just try to carrot users to not do things that are leaky. Which is totally why we have allocated… uh… yeah. Okay we're trying! :-)

As a note though, I still think the exception pointed out in the OP needs to be caught and handled. Under no circumstances should it be possible to raise a NonFatal exception within the runloop and have it result in the house burning down.

All 3 comments

I don't understand why IOShift doesn't already catch this exception type. Seems like an oversight.

I just ran into this today and it was mighty confusing.

It's a shame the types don't prevent you from accidentally using a resource outside of its use. I'm not sure whether we could achieve that without the luxury of fancy session/linear/dependent types. @djspiewak is this something you are looking to address in CE3? I know you are doing some work on monadic regions but I haven't kept up with the details.

It's a shame the types don't prevent you from accidentally using a resource outside of its use. I'm not sure whether we could achieve that without the luxury of fancy session/linear/dependent types.

It's definitely impossible (it follows relatively directly from the proof that linear logic cannot be embedded within predicate calculus). :-( Intuitively, the problem here is the fact that use has type (A => F[B]) => F[B]. Note that the B can be absolutely anything. This in turn means that you're always free to engineer a leak by composing use and pure.

If we were able to restrict B to be something less, then we could get provable resource safety (i.e. encoded in the types), but it would come at the cost of our domain. Oleg's original "monadic regions" paper actually does this. They devised a construct which behaves a bit like Resource in some respects (it's a dynamic region which automatically distributes over flatMap), and they do get provable safety out of it, but they were only able to do this by restricting the value domain to just the concrete algebra they had vetted.

Conceptually, we want to be able to express something like:

Resource[F, A] => (A => F[B]) => F[B] where B doesn't contain anything from any of the Resource values which were constructed via make.

Of course we… can't say that. At least not without making B be something very concrete, which is what Oleg did.

So it's a tradeoff. It's worth noting that bracket has this same problem. In a lot of ways, the best we can do here is just try to carrot users to not do things that are leaky. Which is totally why we have allocated… uh… yeah. Okay we're trying! :-)

As a note though, I still think the exception pointed out in the OP needs to be caught and handled. Under no circumstances should it be possible to raise a NonFatal exception within the runloop and have it result in the house burning down.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

vasilmkd picture vasilmkd  Â·  9Comments

jatcwang picture jatcwang  Â·  4Comments

durban picture durban  Â·  4Comments

Daenyth picture Daenyth  Â·  3Comments

jchapuis picture jchapuis  Â·  6Comments