-
Notifications
You must be signed in to change notification settings - Fork 513
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
Uncancelable boundaries compose poorly #1744
Comments
I'm going to come out and say it: we should break the Functor laws here.
does not print "surely impossible".
Unfortunately this is entirely untenable. The very first use case that got me thinking about this problem is using semaphores safely, i.e. Ultimately I think the goal is producing the model that is easy to reason about: cats laws are generally helpful for that, but in this case they force into a highly unintuitive, highly unpredictable model, and ultimately the concurrency and resource safety characteristics of As a mostly useless philosophical point, we can also conclude the interruptible binds break the Functor laws by definition
yeah, but nothing prevents us from checking the interruption status at every bind, in which case the situation would be exactly the same |
I'll respond more in a bit, but just quickly pointing out… Making interruption visible on each bind would not violate the functor laws at all. What would be a more blatant violation is making it visible every-other bind, because now you can shift semantics by adding identity transformations. I called out our case because we are breaking the laws, but you would need to add 512 identities to see it. This case with uncancelable is very, very easy to see. In fact, I believe your original motivating example actually has a small map at the end of the region, basically demonstrating my point. Can you elaborate further on semaphore? I believe the case I'm talking about is only relevant when there are holes in the subregion, which acquire does not have. |
What if we (I) benchmark making interruption visible every iteration? |
You can do that fairly directly by adding |
I'll think about it. |
that's sort of orthogonal though, having a safe interruption model would break that law anyway I think |
Well, yes and no. As an matter of implementation, if we just artificially saw cancelation constantly at every interleaving point, then we wouldn't need the trailing check on |
spoke about this in private convos, but leaving it here for clarity. Imagine a bounded structure (e..g a queue) implemented with a normal queue and a semaphore, where you essentially need these semantics:
that basically translates to:
the inner |
I think I'm basically convinced that it's better to not do the check at the end of the |
Okay I have a start pushed to It's very likely that I'm going to be very intermittent for the next few days, so if anyone wants to take this and run with it, go forth! |
The motivating example here:
If we imagine canceling the above where cancellation is observed within
fb
, the cancellation will be suppressed until the end of the inneruncancelable
(withinfa
) and then immediately become visible before the end of thepoll
, meaning thatfc
will not run despite the fact that there's no apparent "gap" between the end of theuncancelable
and the end of thepoll
.The reason this is visible is to abide by the functor laws. Specifically, consider the following pair of expressions:
Per the functor laws, these must be equivalent. Thus, cancellation must be visible at the end of an
uncancelable
block just as it would be at the end of anuncancelable
block + some identitymap
orflatMap
action. (note: there's some wiggle room here due to the soft nature of external cancellation and the fact that we're allowed to let the signal remain unobserved for some time, but internal cancellation makes this brutally and immediately apparent)Unfortunately, this kind of visibility makes all sorts of things harder or impossible to express.
allocated
onResource
becomes particularly dangerous, since you can't guarantee that you're able to get at the results of theallocated
before some cancellation is made visible, and the results contain a finalizer which would not have been run within theuncancelable
block (since the cancellation would not be visible at all until after such a block). This scenario commonly arises withResource.make
(where thepoll
is ignored). This probably means thatAsync[Resource]
in its present form has some tiny bits of unsound finalizer semantics littered around its implementation.In general, it's just not possible to trust, given an arbitrary expression
F[A]
, that one of the following always holds:fa
produces the appropriate cleanupfa
cannot be observed within some enclosing expression prior to that expression's first opportunity to handle it without lossThis is violated because
fa
in the above internally handles cancellation to the best of its ability (by wrapping its own critical region), but the outer expression is unable to handle cancellation before the point at which the results fromfa
are lost, which is precisely whatfa
was trying to avoid by wrapping itself inuncancelable
.I can think of a couple ways of addressing this issue:
map(identity)
calls in a row, which seems... improbable.onCancel
in some fashion which allows it to detect the case where a value is produced despite cancellation. This runs afoul of the same functor law issue though in very short order, since we have to answer the question of what it means for anuncancelable
to finish successfully, then getmap
ped once, then hit this hypotheticalonCancel
.uncancelable
to optionally carry a "canceled despite completion" finalizer which would be passed the result (or presumably, the error). This at least seems theoretically tractable, though it probably re-raises the same composability issues asbracketCase
. Also it's not clear it really solves the problem, since there would still be a gap wherein cancellation could be lost.Poll[F]
as a parameter on signatures which would normally have an inneruncancelable
. SoResource#allocated
, for example, would take aPoll[F]
rather than having its ownuncancelable
. That's not necessarily safe though because it kind of depends on the "good will" of the callerPoll[F]
as a parameter and then composing it with an innerPoll[F]
. In other words, instead ofuncancelable(poll => poll(uncancelable(poll => poll(fa))))
(where the inneruncancelable
is likely in a different function), we would carrot people to do something likeuncancelable(outer => uncancelable(inner => outer(inner(fa))))
. This would be an appropriate step to take for any subexpression which produces results which must be considered critical (e.g.racePair
is dodgy).None of these are great. All of them involve resource leaks, and the one which has a special case in which resources don't leak achieves this by violating algebraic identities... and still leaks resources if people (reasonably) assume those identities hold. I think the last of them is probably the most achievable.
Btw this whole discussion is related to #1027
The text was updated successfully, but these errors were encountered: