Lossless cancelation and joinOrCancel - #4641
Conversation
The sys.error call is a side-effect and should be suspended in IO
- add polling cancelable. This is needed to safely start the join in `joinOrCancel` without introducing a cancelation boundary that could drop an already started fiber - replace unsafe usages of `join.onCancel(cancel)` construct with `joinOrCancel` - replace fromCompletableFuture with an implementation that uses cancelable
In IO, cancelable can be implemented without hoisting the operation to a separate thread, by invoking the callback when cancelation is requested. Partly based on Arman's previous attempt in typelevel#3491 Co-authored-by: Arman Bilge <armanbilge@gmail.com>
IO.racePair has to be uncancelable because cancelable introduces a cancelation boundary when used unmasked
2596350 to
2a9e3b6
Compare
|
I have tested this on the http4s test suite. Getting timeouts in |
|
Narrowed issue down to fs2-io. Unix socket tests in there fail with the changes. |
- cancelable needs to be on a separate fiber since it is for blocking operations which do not suspend the fiber, so we cannot start the acks - onCancelRequested becomes a synonym for onCancel by default. In IO, onCancelRequested masks. This lets the implementation keep the `poll(fa).onCancelRequested(ack)`, which will at least cancel in other Fs, but lose data, while working correctly in IO. - cancelable now uses onCancelRequested instead of onCancel so that cancelable operations get a chance to terminate.
The current onCancelRequested doesn't work with it. It looks like it should be fixable without it, by waiting to transition to Unevaluated until the fiber completes.
Needed to preserve bin-compat.
|
Revised things, and updated the main description. I've broken back out a separate |
|
To make |
- fix up naming issues - restore cancelable tests - adjust scaladoc
I don't really understand this. Why does this mean the old behavior is required? |
My solution avoids this by requiring control of the runloop to start the finalizers, and completes them before running any It's probably possible to write an algorithm to safely perform step 3, but that's going to have to be very carefully designed to avoid races. IMO, that's not worthwhile for the relatively few pieces of code that need |
Context
There are a number of open bugs relating to data loss during races:
Fiber#joinOrCancel#4620Fundamentally, these all must occur because there is no way to do all of the following together:
In particular, the fiber that started the other fiber doesn't know it's getting canceled until it observes cancelation (ie.
onCancelruns). At this point it is no longer possible for the fiber to complete, it must cancel. So all it can do is terminate its child fibers and carry on1.I had previously tried to solve this problem with a new
onCancelRequestedcombinator (#4633 ), but that would break otherFs. This implementation instead follows @djspiewak's suggestion to base the solution on an @armanbilge'scancelablefix in #3491 which would givecancelabledifferent behavior inIO, but also leave a working (but not ideal) implementation for otherFs already.New Behaviour
Unfortunately, we still need the old
cancelablebehavior, as it is needed to cancel blocking operations on a fiber (as in literallyF.blocking). This design only works on a suspended fiber. What we can do, while letting otherFs remain no more broken than they are today, is implementonCancelRequestedasonCancelby default, then inIOimplement it by masking the operation, so it can only be canceled by theonCancelRequestedeffect.By adding this behavior, we can run some finalizers before the fiber observes cancelation. Now we can give the operation a chance to return a result before the fiber is canceled and unable to return a result, but also try canceling the operation to make sure cancelation does actually happen if we can't complete.
Changes to Use This Behaviour
Unsafe usages of
join.onCancel(cancel)are replaced with usages ofjoinOrCancelso that they can become lossless inIO. In IO,joinOrCancelwill result in exactly one of getting outcome of the fiber or the fiber being canceled2.onCancelRequestedremains lossy by default, but this is unfixable without new semantics which would break compatibility.The implementation of
cancelableis updated to useonCancelRequested.The implementation of
fromCompletableFutureis also updated to useonCancelRequestedinstead of it's bespokecontimplementation. This implementation is no better by default, but will no longer lose data inIO.Implementation
IOFiber
onCancelRequestedinIOis now handled as a new primitiveIO.OnCancelRequestedclass. This operation introduces a second finalizer (referred to as acks to differentiation from the actual finalizers) stack toIOFiber. When the fiber receives a cancelation request, all acks are immediately run in parallel and any acks that get added after this point are also immediately started. This behavior is intended to drive the fiber towards cancelation as quickly as possible, with the expectation that acks are safe to run at any time, unlike finalizers, which clean up resources. The fiber waits for all acks to complete before running finalizers, since the acks could depend on a resource that will be disposed by a finalizer. If theIO.OnCancelRequestedwould come off the stack, the fiber will also wait on the ack to complete3IO.racePairis also updated to useonCancelRequestedinstead of async cancelation so it does not lose data during a race, following @armanbilge's earlier implementation.Footnotes
You can do a little better if you know the fiber is returning a resource, and clean up that resource, but you still can't get data back out. ↩
or non-termination, of course. ↩
The ack should already be completed, since the ack cancelable action should ↩