-
Notifications
You must be signed in to change notification settings - Fork 537
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
Cats Effect 3.0 Interruption Model Proposal #681
Comments
I have some questions regarding the
One possible way to avoid ambiguity would be to remove class Poll {
def apply[B](fa: F[B], finalizer: F[Unit]): F[B]
} |
You can't do that because |
@SystemFw mask { poll =>
poll(F.never).guarantee {
F.delay(println("Waiting 1s")) >>
F.sleep(1.second) >>
poll(F.unit) >>
F.delay(println("Waited 1s"))
}
}.start.flatMap {
F.sleep(1.second) *> _.cancel
} Will it ever print |
So, that code is using the two things whose semantics are still in the works, i.e:
I'm still trying to define what's best there, in haskell the Fiber would inherit the masking status unless you use What are your ideas here? |
I think we can extract at least one clear question from there, which is:
My first thought is that this is a no-op, things inside |
If you look closely, the So actually, my snippet isn't about inheritance of masking status, it's about "stickinness" of interruption – can a single |
Yeah, sorry about that :)
And I'd say that that's a no-op and things inside onCancel are uninterruptible. What do you think? |
@SystemFw Re: usage of |
We could actually make it possible by encoding a functional dependency within Anyway, I think your proposal is more realistic: just detect leaked |
So after pondering this quite considerably… For all of the reasons you outlined, I really don't believe it is possible in general to write code which is generic across both continual and auto-cancelable evaluation models. It simply isn't. If you write the code in a fashion which is amenable to auto-cancelability, then you will have deadlocks when evaluated in a continual model and vice versa with resource safety. Basically, all effectful code makes some sort of implicit assumption about the cancelability of its logic, and when that assumption is violated, bad things can happen. Technically, For that reason, I think we scrap the goal of unifying the two models and simply focus on auto-cancelable evaluation. To that end, I'm officially in favor of your proposal as broadly outlined in the OP, though I don't like the name |
I now have this sketched out locally. I'm going to close this since I think we're pretty definitively settled on this model. I'll try to have things up on the branch this week. Thank you SOOOO much @SystemFw for all your work on this! |
…monad, vim dotty Rock Paper Scissor Game through Actor Model in Scala https://medium.com/@karthikv1392/rock-paper-scissor-game-through-actor-model-in-scala-a8a1430f10ff Sizeof in Scala http://yannmoisan.com/sizeof-in-scala.html) using `import org.apache.spark.util.SizeEstimator._` Scala Classes – Syntax, Declaration, Use case, Examples https://leobenkel.com/2019/10/scala-classes How to keep your sanity working with Scala Implicit! https://medium.com/@lprakashv/how-to-keep-your-sanity-working-with-scala-implicit-fda9ffc33162 Giter8 - Part 3 - Clean Architecture Use Case Scaffold https://www.youtube.com/watch?v=RQU74SHNB08 Cats Effect 3.0 Interruption Model Proposal typelevel/cats-effect#681 Hacking with scala circe-json https://medium.com/rahasak/hacking-with-circe-json-scala-ca626705733e Open-sourcing Polynote: an IDE-inspired polyglot notebook https://medium.com/netflix-techblog/open-sourcing-polynote-an-ide-inspired-polyglot-notebook-7f929d3f447 Websockets in Scala using sttp https://blog.softwaremill.com/websockets-in-scala-using-sttp-baefd207c5fb Journey into the IO Monad (Part 1) https://medium.com/@RaymondTayBL/journey-into-the-io-monad-part-1-bdf591b4fa09 Journey into the IO Monad (Part 2) https://medium.com/@RaymondTayBL/journey-into-the-io-monad-part-2-2e21da2826e3 Journey to the IO Monad (Part 3.1) https://medium.com/@RaymondTayBL/journey-to-the-io-monad-part-3-1-35813c7f48ac Vim with Dotty (using coc vim) https://www.dev-log.me/Coc_Vim_with_Dotty
I'm writing this proposal to describe the interruption model I'd like to see for cats-effect 3.0.
I'm only going to be focusing on semantics, things like the place in the hierarchy should be discussed elsewhere.
The main goals of this proposal are to lower the skill level needed to write interruption- and finalisation- safe code, and to preserve as much compositionality as possible in a problem that at some level defies it.
I'm going to try setting enough context to justify the decisions in this proposal, if you'd like to skip to the core of it, go to the
Combinators and Semantics
section.The problem
The notion of safety in concurrent code is not univocal, and often revolves around two separate concepts: finalisation and interruption.
Finalisation is needed to ensure that certain sections of the code will run in any scenario: for example to close resources, or restore some data to a valid state.
Interruption is needed to avoid using resources once the calling code has decided they are no longer needed, to prevent deadlock by offering timeouts, and to compositionally express several patterns (e.g. do this for 10 seconds, then start doing that).
Unfortunately, these two concepts are at odds at a fundamental level :
From a theoretical point of view, there is no perfect compromise: by prioritising resource safety at all costs, you lose all interruption (this is current
uncancelable
), and by prioritising interruption atall costs, you lose resource safety (this is your standard
flatMap
).However, I will posit that for our users, loss of resource safety is more dangerous than loss of interruption, and therefore we shall keep things as interruptible as possible, but prioritise resource safety when breaking a tie. You will hopefully see exactly what I mean by this, further down in the proposal.
Current situation
I think it's safe to say that currently writing resource safe code that preserves interruption is a matter for experts, for two reasons:
The first is that users need to be familiar with
guaranteeCase
,bracketCase
,continual
,uncancelable
, and the interaction of those withjoin
andcancel
. Each operation has subtly different semantics when it comes to interruption and they are all absolutely necessary in some scenarios.The second is more fundamental: writing interruption-aware code means basically writing code that restores some properties when interruption happens. How and what needs to be restored generally depends on where interruption happened, and there is no way of knowing that.
In practice this often means having to do complicated reasoning over a piece of state and figure out how to write code which can always restore it to a "steady" state, which can get very complex.
Principles
not. The writer can allow interruption in limited places, but cannot have a guarantee that the final code will be interruptible, it's up to the caller.
In other words, it's possible for someone to call code that was designed to be interruptible and make it uninterruptible. This is unavoidable really (if we want principle 2), but I'll argue it's the
correct default. Also, preserving interruptibility is fairly easy, unlike today.
Combinators and Semantics
Safe regions
The model can be described by two functions (names can be changed):
onCancel
attaches a finalizer to anF[A]
. Upon interruption, all registered finalizers are run and the rest of the computation is short-circuited. The code triggering interruption backpressures onsaid finalizers.
mask
allows to control interruption in critical sections by defining a region in which interruption cannot happen unless one explicitly encloses the code that can be interrupted in a call toPoll.apply
.Because the writer of the code now knows exactly at which points interruption can happen, it's easy to add the correct logic via
onCancel
. We will soon see that the semantics ofPoll.apply
need to be refined a bit, but this is a good first intuition.Example:
Which means:
Note that
p1
is almost the same asbut there is a crucial difference, in the
bracket
version.flatMap
andmodifySomeState...
can be interrupted, which means the code now has to worry about restoringSomeState
, without knowing ifinterruption happened onwaitOnCondition
, or in between, or in any of theflatMaps
that might make upmodifySomeState...
, requiring complex reasoning to figure out what to restoreSomeState
to.I hope this illustrates how much nicer this model is to work with, as you essentially get to build a safe region where you need one.
Note 1:
In the snippet above I simply
attempt
ed any synchronous errors for brevity, but all sorts of error handling are possible of course.Note 2:
In cats-effect 2.0, you have to
bracket
andcontinual
to writep1
correctly.Nested regions and restore semantics
So, in the section above we have defined
Poll.apply
, which is called in an uninterruptible region defined bymask
, as making its argument interruptible again.We will now see how these semantics are slightly wrong and require some refinement in order to comply with the principles above.
Imagine
p1
is being called byp2
, which is also safety-aware:Now,
p2
thinks it's safe, but it's unaware that insidep1
there is an interruptible region defined by a call topoll
, and so it can now be interrupted and not executecloseResource2
.In other words, writing resource safe code requires inspecting all downstream code to know whether a
flatMap
is safe, which in my opinion is not an acceptable solution.We can solve this problem by changing the semantics of
Poll.apply
: a call toPoll.apply
makes its argument inherit the interruption status of the outer scope.For example, if we have
def main = p1
, the outer scope interruption status isinterruptible
(by default things are interruptible), and insidep1
waitOnCondition
inherits this status, i.e. it'sinterruptible, and we have recovered the previous semantics.
However, when nested inside
p2
, the outer scope isuninterruptible
since we are insidemask
, and so the call topoll
insidep1
inherits that, and things are safe again.If
p2
wants to allow interruption ofp1
, it can callpoll
again:p1.poll
inherits the status ofp2.poll
, which inheritsmain
, meaning thatwaitOnCondition
is now interruptible again, but none of the other parts are, which are the desired semantics. This extends to arbitrary nesting.Let's highlight where the tradeoff lies: the caller of code written in
mask
can take code that is safely interruptible, and (perhaps accidentally) make it uninterruptible by not callingpoll
on it.Or, to look at it from the other side, the writer of code in
mask
cannot guarantee that their code is interruptible, it can only allow for it by callingpoll
, but the caller will have the final say.I think this is the correct tradeoff: as the caller, you know whether you care about interrupting one of the things you call or not, so the failure mode of forgetting to call
poll
is way less dangerous than the initial semantics, where the code you write can become interruptible because something way down in the callstack has used an interruptible region.I think this as compositional as we can get, and hopefully explains principles 2 and 3, and clarifies what I mean by saying that on balance, resource safety is favoured.
Relationship with
bracket
etc.We don't have to rewrite
bracket
and co., and actually we could also foregoonCancel
and useguaranteeCase
, but I'll point out for the sake of argument how our native combinators can be easily expressed in this model.This should also serve as a small example section:
Differences with Haskell
This model is obviously inspired by Haskell's async exceptions, but there is a huge difference: in haskell some blessed operations are interruptible even inside
mask
, for exampleMVar.take
.As argued above, I think this is not a great default in general, and it's even worse in cats-effect because things like
MVar
aren't built in and users can create their own blocking primitives.Another obvious difference is that
onCancel/guaranteeCase
aren't the same ashandleErrorWith
, so you cannot catch interruption, whereas in haskell you can and you need to remember to rethrow.Open questions on
start
There is an open question about
start
: the Haskell's equivalent (forkIO
) has the spawned fiber inherit the interruption status ifforkIO
is called insidemask
, with another primitiveforkIOWithUnmask
to achieve independence.I still haven't made up my mind as to which semantics are necessary or desirable here. The other questions are around what happens if users pass a
Poll
instance around, but I think those shouldn't constitute a blocker to the main drive of the proposal.Thanks
Sorry for the wall of text! I did my best to try and condensate this immensely tricky problem to its essence, but it still turned out super long.
The text was updated successfully, but these errors were encountered: