-
Couldn't load subscription status.
- Fork 1.4k
Make use interruptible in IO.bracket0
#270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make use interruptible in IO.bracket0
#270
Conversation
| Ref[Option[A]](None).flatMap { m => | ||
| for { | ||
| f <- acquire.flatMap(a => m.set(Some(a)) *> use(a).fork).uninterruptibly | ||
| b <- f.join.ensuring(m.get.flatMap(_.fold(IO.unit)(a => f.observe.flatMap(r => release(a, r))))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ensuring needs to go around the outermost block. Not around join, because if it's around join, then if join does not start executing (it's possible—the action can be interrupted right after the acquire block completes), then the finalizer will not execute. So move the ensuring to the outside of the for loop. This implies you need to put the fiber in the Ref, too.
Maybe something like:
Ref[Option[(A, Either[Fiber[E, A], B])]](None).flatMap { m =>
(for {
f <- acquire.flatMap(a => use(a).fork.flatMap(f => m.set(Some(a -> Left(f)) *> IO.now(f)))).uninterruptibly
b <- f.join
_ <- m.update(_.map(t => (t._1, Right(b)))) // We have the `b`, so we don't need the fiber anymore
} yield b).ensuring {
m.get.flatMap(_.fold(IO.unit) {
case (a, Left(fiber)) => fiber.observe.flatMap(release(a, _))
case (a, Right(b)) => release(a, ExitResult.Completed(b))
})
}
}This is not quite correct. If you add supervised it won't really help.
We need something like tryObserve or isDone added to Fiber, so we can check to see if the fiber is done at the moment when the ensuring finalizer is being executed. If the fiber is not done, it means the main fiber is being interrupted, so we should interrupt the child fiber. If the fiber is done, then it means we should observe and use the observed exit result on the release action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😮 that's brilliant! Do we need the Left/Right ceremony? A tryObserve should be super fast, synchronous even. I've submitted a new version to let you see; will work on tryObserve now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure, maybe you can eliminate it. The important thing is to pass the b to release if use succeeds.
|
This now works 🎉 Thank you for your comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking great! ❤️ Just a few minor cleanups and it should be good to go. Awesome work! 💪
| } | ||
| self.observe.seqWith(that.observe)(combineExitResults) | ||
|
|
||
| def tryObserve: IO[Nothing, Option[ExitResult[E1, C]]] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest self.tryObserve.seqWith(that.tryObserve)(???)
| def interrupt0(ts: List[Throwable]): IO[Nothing, Unit] = | ||
| self.interrupt0(ts) *> that.interrupt0(ts) | ||
|
|
||
| private def combineExitResults: (ExitResult[E, A], ExitResult[E1, B]) => ExitResult[E1, C] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about moving this method to ExitResult, and calling it zipWith? e.g. e1.zipWith(e2)(f(_, _))?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Consistent vocabulary and behaviour across the whole API ❤️
| f.tryObserve.flatMap { | ||
| case Some(r) => release(a, r) | ||
| case None => f.interrupt | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks correct to me! (Of course I may have said that before. 😆)
Getting information out of the use action while still preserving its interruptibility is amazingly difficult with only ensuring and uninterruptible as primitives. At least the more common version of bracket is going to be much faster (IO.bracket), and it looks correct to me, too (using the same strategy, but no need to fork).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least the more common version of bracket is going to be much faster
I still don't have a good intuition of the performance impact of using one version vs another, I mean:
- a fiber is just a few hundred bytes, forking must be super fast
- joining has virtually no cost since it merely adds a callback to a list
tryObserve(should we call itpoll? likePromise.poll) is super fast
If my reasoning is correct, that leaves interruption, which may block the thread indeed.
| } | ||
| def tryObserve: IO[Nothing, Option[ExitResult[Throwable, A]]] = IO.sync { | ||
| ftr.value match { | ||
| case Some(Success(a)) => Some(ExitResult.Completed(a)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can simplify with ftr.value.map { case Success(a) ... case Failure(t) ... }.
Rewrites
IO.bracket0following @jdegoes 's description here.It's not quite there yet but I'm getting closer.