-
Notifications
You must be signed in to change notification settings - Fork 779
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
Fix 3219 - make StartAsTask wait for cancellation to take effect on task #3256
Conversation
@matthid, |
Start(token, a) | ||
queueAsync | ||
token | ||
(fun r -> tcs.SetResult r |> fake) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@matthid shouldn't this be using the Try* methods instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why? It should be guaranteed that only a single continuation is taken?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not rely on that assumption TBH
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you have some scenario in mind? Because currently I'm thinking: "Just let it crash if someone breaks that assumption"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not rely on that assumption TBH
My understanding is that in this situation you can rely on only one continuation being called.
token | ||
(fun r -> tcs.SetResult r |> fake) | ||
(fun edi -> tcs.SetException edi.SourceException |> fake) | ||
(fun _ -> tcs.SetCanceled() |> fake) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably should even use (fun exn -> tcs.SetException(exn))
.
I think consumers don't really see a difference and it unifies StartAsTask
and RunSynchronously
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it looks like SetException
is actually the right thing to do:
https://github.com/dotnet/coreclr/blob/68f72dd2587c3365a9fe74d1991f93612c3bc62a/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs#L670
It will set the task to "canceled" internally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See also how this is potentially changed in #3257
let tcs = TaskCompletionSource<unit>() | ||
let a = async { | ||
cts.CancelAfter (100) | ||
do! tcs.Task |> Async.AwaitTask } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It makes me a bit nervous that I don't see a way to remove this race condition (ie calling cts.Cancel
after the bind)
bf40ea3
to
eedeb09
Compare
I had to rebase on top of #3255 because otherwise the new tests actually hit the fixed bug... |
@matthid @KevinRansom I'm labeling this as WIP - I definitely want much more review on this one. Reading through #3219 it is not yet at all clear to me the various options for resolution (and there tradeoffs against leaving things as they are, for example) |
@dsyme I'm not sure what you mean. IMHO with current behavior it's very hard to write correct code. Especially if you don't understand exactly what it did before this change, it is very likely that most users of this API have race conditions in place... |
At least I'd like to point out that I currently don't see any action I could do to make this a non WIP. Can we at least clarify who needs to do what? |
We need multiple review approvers and plenty of time to think about it. WIP is as good a way as any of saying "really do not merge yet" |
I don't know what to do, lets wait for @dsyme. Apparently we need more review on this? |
src/fsharp/FSharp.Core/control.fs
Outdated
@@ -1201,7 +1184,7 @@ namespace Microsoft.FSharp.Control | |||
args.aux.trampolineHolder.Protect((fun () -> | |||
if completedTask.IsCanceled then | |||
if useCcontForTaskCancellation | |||
then args.aux.ccont (new OperationCanceledException(args.aux.token)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I thought this change was done in a separate PR (or we decided not to do it in a separate PR....)
src/fsharp/FSharp.Core/control.fs
Outdated
@@ -1216,7 +1199,7 @@ namespace Microsoft.FSharp.Control | |||
args.aux.trampolineHolder.Protect((fun () -> | |||
if completedTask.IsCanceled then | |||
if useCcontForTaskCancellation | |||
then args.aux.ccont (new OperationCanceledException(args.aux.token)) | |||
then args.aux.ccont (new OperationCanceledException()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Likewise this one?
This looks fine to me, apart form the fact that these two changes look unrelated? https://github.com/Microsoft/visualfsharp/pull/3256/files#diff-34ee82347e9e0b47265fcecaa4850174L1204 I'd also like additional approvers |
@dsyme the changes are an artifact because you squash merge here and git can no longer recognize the change is already in the repository :/ |
I can try to get rid of it if you want. |
Oh, yes, I see. But yes, please get rid of them, makes me more comfortable |
Ok I force pushed the rebase. I can see why you are kind of hesitant to merge this. It really is a bit scary when a complicated primitive simplifies like this... |
queueAsync | ||
token | ||
(fun r -> tcs.SetResult r |> fake) | ||
(fun edi -> tcs.SetException edi.SourceException |> fake) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any way to maintain the stack trace here, e.g. some kind of tcs.SetExceptionDispatchInfo
? I presume we lose the stack trace right now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it worse than before?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically I think Task will take care of not loosing info (wrap the exception in an aggregateexception or take care while rethrowing)
I don't think we have enough access to the primitives to do something more useful (there are in fact some framework internal interesting apis)
Well, the code looks much better. But yes, more eyes are needed |
It is a bit sad that we can't trust our test-suite on this. But I definitely agree - with all the stuff we fixed in that area lately - we can't trust it... |
I think @tpetricek once said "if it's not simple it's impossible" ;) |
Could you add a test for the stack trace we get? e.g. check the names of the methods on the stack? And check against existing behaviour too please, thanks |
Also compare with #2534 please |
@dsyme the callstack in an exception is not really part of the contract. Why is that interesting? |
@KevinRansom That's very philosophical :) Though good stack traces are interesting, regardless of whether they are part of the contract :) Anyways.... all the "ExceptionDispatchInfo" work in control.fs exists to give better stack traces. We somehow need to validate that we are not degrading that here. I personally prefer to put this under CI test and adjust should something change in .NET (which it is unlikely too in debug mode). |
@dsyme From my understanding the Also I will provide the test but keep in mind that Stacktraces in async code are basically quite useless and they always where (even with the latest DispatchInfo additions). Everyone using async knows this, so please lets fix this but not as part of this bugfix. |
The bar for changes is always high and I'd like us all to work together to make it much higher. I am doing what I want you to do to me :) I want more and more push back from all reviewers, especially given the fact we've introduced occasional regressions recently. That said, I took a look at the code again and what you say is reasonable - the thing that had made me request this was the elimination of the EDI here:
But as you say this EDI is newly introduced
The important thing is that we don't regress, and that we improve where possible. The EDI changes improved some situations dramatically, though not enough as any of us would like. |
@matthid Given what you say above (SetException was called before) a CI test isn't required |
The really important question to answer is about the change of behavior: As you can see from the linked issue my argument is that the previous behavior doesn't make any sense and cannot be used to write correct code. While there are arguments for the other behavior I don't really share any of them. Also another hint that this is the "correct" change from a design perspective is that you cannot really achieve this primitive (as it is with this path) in user code, but you can very easily achieve the "old" behavior with this (see linked discussion). Once we agree on the answer on this we should discuss potential breaking code (which IMHO always means the code is basically already broken right now if it depends on this behavior) |
I'm ok with the change of behaviour "make StartAsTask wait for cancellation to take effect on task", it is the intended behaviour for StartAsTask
It sounds like an RFC discussion, to discuss the breaking change. That would be the appropriate route to permanently document a change in behaviour. Alternatively, open a specific new issue on this and document the exact case there, or do it in the description of this PR? |
We already have the discussion issue. For my part I really see it as bugfix to restore the intended behaviour (it seems you do as well), it's really up to you guys on how far you want to take that (I just wanted to be fair regarding to the critical voice of @eiriktsarpalis in the discussion) |
@matthid Yes, thanks. Stepping back, I must say I basically very much prefer the implementation you propose. It makes total sense to me - it very much aligns with StartWithContinuations, and StartAsTask should basically be implementable in terms of a TCS and StartWithContinuations, which is effectively what you are doing. And that is also aligning with the proposed implementation of StartImmediateAsTask. When I saw it, I had no idea why the existing implementation was so contorted - and, as you point out, dubious under cancellation (nb. I didn't write the original code for StartAsTask) |
This fixes that
StartAsTask
will not wait properly for cancellation to finish, see #3219 (comment)/cc @eiriktsarpalis