Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ type AsyncType() =

member private this.WaitASec (t:Task) =
let result = t.Wait(TimeSpan(hours=0,minutes=0,seconds=1))
Assert.IsTrue(result)
Assert.IsTrue(result, "Task did not finish after waiting for a second.")


[<Test>]
Expand All @@ -147,8 +147,39 @@ type AsyncType() =
Async.StartAsTask a
this.WaitASec t
Assert.IsTrue (t.IsCompleted)
Assert.AreEqual(s, t.Result)
Assert.AreEqual(s, t.Result)

[<Test>]
member this.StartAsTaskCancellation () =
let cts = new CancellationTokenSource()
let tcs = TaskCompletionSource<unit>()
let a = async {
cts.CancelAfter (100)
do! tcs.Task |> Async.AwaitTask }
Copy link
Contributor Author

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)

#if FSCORE_PORTABLE_NEW || coreclr
let t : Task<unit> =
#else
use t : Task<unit> =
#endif
Async.StartAsTask(a, cancellationToken = cts.Token)

// Should not finish
try
let result = t.Wait(300)
Assert.IsFalse (result)
with :? AggregateException -> Assert.Fail "Task should not finish, jet"

tcs.SetCanceled()

try
this.WaitASec t
with :? AggregateException as a ->
match a.InnerException with
| :? TaskCanceledException as t -> ()
| _ -> reraise()
System.Diagnostics.Debugger.Break() |> ignore
Assert.IsTrue (t.IsCompleted, "Task is not completed")

[<Test>]
member this.StartTask () =
let s = "Hello tasks!"
Expand Down
35 changes: 9 additions & 26 deletions src/fsharp/FSharp.Core/control.fs
Original file line number Diff line number Diff line change
Expand Up @@ -974,33 +974,16 @@ namespace Microsoft.FSharp.Control
let tcs = new TaskCompletionSource<_>(taskCreationOptions)

// The contract:
// a) cancellation signal should always propagate to task
// b) CancellationTokenSource that produced a token must not be disposed until the task.IsComplete
// We are:
// 1) registering for cancellation signal here so that not to miss the signal
// 2) disposing the registration just before setting result/exception on TaskCompletionSource -
// otherwise we run a chance of disposing registration on already disposed CancellationTokenSource
// (See (b) above)
// 3) ensuring if reg is disposed, we do SetResult
let barrier = VolatileBarrier()
let reg = token.Register(fun _ -> if barrier.Proceed then tcs.SetCanceled())
// a) cancellation signal should always propagate to the computation
// b) when the task IsCompleted -> nothing is running anymore
let task = tcs.Task
let disposeReg() =
barrier.Stop()
if not (task.IsCanceled) then reg.Dispose()

let a =
async {
try
let! result = computation
do
disposeReg()
tcs.TrySetResult(result) |> ignore
with exn ->
disposeReg()
tcs.TrySetException(exn) |> ignore
}
Start(token, a)
queueAsync
token
(fun r -> tcs.SetResult r |> fake)
Copy link
Member

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?

Copy link
Contributor Author

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?

Copy link
Member

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

Copy link
Contributor Author

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"

Copy link
Contributor

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.

(fun edi -> tcs.SetException edi.SourceException |> fake)
Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor Author

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)

(fun _ -> tcs.SetCanceled() |> fake)
Copy link
Contributor Author

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

computation
|> unfake
task

[<Sealed>]
Expand Down