Repository navigation
Lossless cancelation - #4710
Draft
reardonj wants to merge 19 commits into
Draft
Lossless cancelation#4710reardonj wants to merge 19 commits into
reardonj wants to merge 19 commits into
Conversation
reardonj
force-pushed
the
4620-onCancelContinueWith
branch
2 times, most recently
from
November 4, 2026 04:49
6bb7389 to
cfd23db
Compare
reardonj
force-pushed
the
4620-onCancelContinueWith
branch
from
November 5, 2026 16:21
3e9db9a to
a56d797
Compare
The sys.error call is a side-effect and should be suspended in IO
- add onCancelContinueWith that can temporarily revert cancelation to MonadCancel and IO - replace cancelation stack with full unwinding to support continuing from cancelation - add equivalent methods for Async - use onCancelContinueWith to make racePair and other combinators cancelation-safe
"parTraverseN - interrupt on errors" failed in CI on: macos-14, 2.13.18, temurin@17, ciJVM Local running of the parTraverse benchmarks don't show an significant change in speed between branch and series/3.x, so I'm chalking this up to flakey CI and reducing repetitions.
PureConc really needs async cancelation to be implemented to get that to work.
Still getting flakey CI. Not able to reproduce locally, so I don't think I'm seeing a real issue.
reardonj
force-pushed
the
4620-onCancelContinueWith
branch
from
November 5, 2026 16:40
a56d797 to
9ac7ec7
Compare
reardonj
marked this pull request as ready for review
November 5, 2026 17:56
Add an explicit test to ensure that the take.timeout.timeout issue is resolved by the racePair fix.
reardonj
force-pushed
the
4620-onCancelContinueWith
branch
from
November 6, 2026 03:31
39482c5 to
2faccee
Compare
reardonj
marked this pull request as draft
November 7, 2026 12:39
Since finalizing can now go back and forth, we can't just use a finalizing flag. Instead, we just track if the cont still has a get pending.
Now that ContState doesn't need to care about cancelation, we can replace the finalizing flag with mask tracking. Also fixes a bug where error handling after unmasking took precedence over cancelation.
reardonj
force-pushed
the
4620-onCancelContinueWith
branch
from
November 10, 2026 03:13
f85904a to
f313ce8
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR provides an alternate solution to #4641 to fix #4571 , #4489, #3474 and related issues.
This solution avoids adding a completely different cancelation mechanism by breaking the fundamental principles of cancelation, by letting us 'continue' instead of unconditionally canceling, allowing graceful cleanup:
onCancelGracefulworks likeonCancelexcept:This let's us write idioms like:
poll(f.join).onCancelGraceful(f.cancel *> f.join)To express that if the fiber is canceled, another fiber,
f, should also be canceled, but still needs to be joined to the current fiber.I've also included
Pollsyntax to capture the idiom, so we can just writepoll.fiberGracefully(f). This is essentiallyjoinOrCancel(#4620)Implementation
onCancelGracefulis defined inMonadCancel, with a default implementation that degrades toonCancel. This results in fairly clean fallback behavior (no worse than in CE 3.7.x) for implementations that don't define their ownonCancelGraceful.This required some reworking of how cancelation works. There is no longer a separate stack of finalizers in IOFiber1. Instead, the stack is unwound until an
onCancelGracefuloronCancelis hit. In either case, the handler is then executed. Then in theonCancelGracefulcase, the fiber continues executing instead of continuing cancelation. However, the fiber is still canceled! So we'll immediately continue unwinding unless cancellation is masked whereonCancelGraceful's handler was run. If cancellation is masked, the fiber will continue running until unmasked, then start unwinding again.I've also written
async*equivalents that complete on cancelation and made use of them in a few places.Compared to #4641
Since
onCancelGracefulstill requirespoll, this design avoids the weirdness of #4641 whereonCancelRequesteddidn't actually need poll, but it was needed to make things work with the bad defaultonCancelRequested.This design is less powerful, as the stack has to get unwound to the handler before continuing, so state that was set up in the poll is lost before the handler goes off. This means usages tend to be a bit repetitive (eg. the
joinexample above), where both the poll body and handler have similar content.On the upside, laws for the new method are fairly obvious, as they mostly follow the
onCancellaws.What is Not Fixed
cancelable, but does not avoid the separate fiber. If anything, this probably pushes us further away from removing the fiber by removing the separate finalizer stack. Since cancelation plays out by unwinding the stack, we really can't run finalizers asynchronously.TODO
AI Use
Some of this code was generated by Copilot, especially test updates.
Footnotes
As a bonus, under
-XX:+UseCompactObjectHeaders/ JVM 27, this fixes Can we push to makeIOFiberfit in 64 bytes? #3584 by pushing the object size to 64 bytes! ↩