-
Notifications
You must be signed in to change notification settings - Fork 781
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
Cancel task continuations when async contexts are cancelled #7357
Conversation
Currently if you transform a task into an async via Async.AwaitTask and then run that async the continuation added to the underlying task is only ever resolved if the task itself resolved (either via completion, exception, or cancellation). Notably if the async created by Async.AwaitTask is cancelled the task continuation isn't. We're seeing this manifest as a large memory leak when we repeatedly run Async.Choice with a task that only resolves if an error elsewhere in the system is hit. Each time we run Async.Choice it appends another task continuation, the async is then cancelled (so if the task did resolve the async continuation would immediately see it was cancelled anyway) but critically the task continuations are not cancelled and so stay allocated. This commit changes the task continuation to now cancel if the asyncs cancellation token requests cancellation. Note that we need to have a second continuation to watch for the case that the first continuation is cancelled and thus never run, the second continuation then triggers the async cancellation or exception continuation as appropriate. This also means that some asyncs will now report cancelled sooner (see the changes to the StartAsTaskCancellation test) while before they would of been blocked waiting for a task to complete before any of their continuations could run.
Thinking about this over the last few days, I'm thinking that if the Async is cancelled (rather than the task) we should just call the cancellation continuation. This isn't the same as #1166 as this would only be when the asyncs cancellation token was triggered, not when just the task was cancelled. |
As I understand it this basically reverts #3256, correct? Can you add some example code on where this happens? So your scenario is basically having some long running (never ending) |
Can't you just use something similar to this in your application to resolve the issue (basically calling let endlessTask = TaskCompletionSource<unit>().Task
async {
let! tok = Async.CancellationToken
do! endlessTask.ContinueWith(Action<Task<'T>>(fun t -> ()), tok) |> AwaitTask
}
|> Async.RunSynchronously |
No this shouldn't be changing the semantics of StartAsTask at all. This is only to change the semantics of the Async created by AwaitTask.
Correct, we have a task that is triggered on error and we repeatedly call that with AwaitTask and Async.Choice (which cancels the asyncs you pass to it as soon as one finishes). Because cancelling the asnyc returned by AwaitTask doesn't cancel until the task is finished this results in huge numbers of task continuations being built up.
Kinda, that won't propagate the result or exceptions from the underlying task though. But yes the fix for this is to have two ContinueWiths one to handle the task completing and one to handle the async cancelling. |
I'm on holiday for 5 days, but will see about adding some tests for this case when I'm back. |
Yes, the function is different but as far as I understand it, the async is now "finished" sooner than the task we await. This is exactly the behavior the linked PR removed (there are several places in the implementation where this is relevant).
Question is if this is kind of "expected" and we just need to document it (in addition to the workaround).
Yes the real fix needs to be slightly different (just calling I'm just afraid that we should not change the One could argue that we need a mentioning @eiriktsarpalis as he warned about this |
Haven't had time to reply properly to this. Still need to sit down and write up some more test, but wanted to get some response out.
As far as I can tell the linked PR was just changing the behavior of StartAsTask, the fact that AwaitTask didn't cancel wasn't changed by that PR. Yes the test has changed, but the test was added to check that the Task from StartAsTask didn't cancel until the async was canceled (rather than triggering the tasks cancel as soon as the token was triggered). I see now that the new test isn't testing the same thing, it needs something to block the cancellation but I don't think that means AwaitTask needs to be blocking.
We certainly didn't expect it. It seems strange to have an Async that rather silently and implicitly doesn't respect cancellation. AwaitEvent has a similar behavior but at least there it's a clear opt in where you have to pass a function to be triggered when cancel happens as a compensation for async cancellation not happening.
Choice also introduces unexpected parallelism. In fact if choice blocked waiting for all cancellations we would of picked up this leak much sooner.
Sure I see how this could be looked at either way. I first came at this thinking the Async is not the Task (and thus can be cancelled independently), but I understand how a lot of people could write it assuming the Async is the Task. I guess it's semantics of have we mapped the Task in Async land, or do we just have an async that happens to be waiting for a task. I say that because you've got things like AwaitEvent which also has to solve this cancellation problem (which it does by the passed in cancel function). I don't think it's Event or Task specific though. If all these Await Asyncs just respected cancellation you could compose them with a BlockCancellation async to get either behavior. BlockCancellation is a pretty lame name but signature wise I was thinking something like Or a variation on simple bools, This compose with things like AwaitTask, where if you want the current blocking till the task is done behaviour you can do
An accurate warning, I'd support Choice awaiting all cancellations just like Parallel. edit A better name is probably CatchCancellation |
@Frassle , there has been no movement on this for almost a year, I am going to close it. Please re-open this PR when you are ready to proceed with it. Thank you for this contribution Kevin |
Sure sounds like this isn't mergable until a decision on #2127 is made. |
@Frassle , okay, when you are ready to reactivate this, we will consider it at that time. Once more thanks for the work you put in on this Kevin |
Currently if you transform a task into an async via Async.AwaitTask and
then run that async the continuation added to the underlying task is
only ever resolved if the task itself resolved (either via completion,
exception, or cancellation). Notably if the async created by
Async.AwaitTask is cancelled the task continuation isn't.
We're seeing this manifest as a large memory leak when we repeatedly run
Async.Choice with a task that only resolves if an error elsewhere in the
system is hit. Each time we run Async.Choice it appends another task
continuation, the async is then cancelled (so if the task did resolve
the async continuation would immediately see it was cancelled anyway)
but critically the task continuations are not cancelled and so stay
allocated.
This commit changes the task continuation to now cancel if the asyncs
cancellation token requests cancellation. Note that we need to have a
second continuation to watch for the case that the first continuation is
cancelled and thus never run, the second continuation then triggers the
async cancellation or exception continuation as appropriate.
This also means that some asyncs will now report cancelled sooner (see
the changes to the StartAsTaskCancellation test) while before they would
of been blocked waiting for a task to complete before any of their
continuations could run.