diff --git a/src/fsharp/FSharp.Core/async.fs b/src/fsharp/FSharp.Core/async.fs index 0d5abef033..23cbf78c0e 100644 --- a/src/fsharp/FSharp.Core/async.fs +++ b/src/fsharp/FSharp.Core/async.fs @@ -921,46 +921,60 @@ namespace Microsoft.FSharp.Control |> unfake task - // Helper to attach continuation to the given task. + // Call the appropriate continuation on completion of a task [] - let taskContinueWith (task: Task<'T>) (ctxt: AsyncActivation<'T>) = - - let continuation (completedTask: Task<_>) : unit = - ctxt.trampolineHolder.ExecuteWithTrampoline (fun () -> - if completedTask.IsCanceled then - let edi = ExceptionDispatchInfo.Capture(TaskCanceledException completedTask) - ctxt.econt edi - elif completedTask.IsFaulted then - let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception - ctxt.econt edi - else - ctxt.cont completedTask.Result) |> unfake - - if task.IsCompleted then - continuation task |> fake + let OnTaskCompleted (completedTask: Task<'T>) (ctxt: AsyncActivation<'T>) = + assert completedTask.IsCompleted + if completedTask.IsCanceled then + let edi = ExceptionDispatchInfo.Capture(TaskCanceledException completedTask) + ctxt.econt edi + elif completedTask.IsFaulted then + let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception + ctxt.econt edi else - task.ContinueWith(Action>(continuation), TaskContinuationOptions.ExecuteSynchronously) - |> ignore |> fake + ctxt.cont completedTask.Result + // Call the appropriate continuation on completion of a task. A cancelled task + // calls the exception continuation with TaskCanceledException, since it may not represent cancellation of + // the overall async (they may be governed by different cancellation tokens, or + // the task may not have a cancellation token at all). [] - let taskContinueWithUnit (task: Task) (ctxt: AsyncActivation) = + let OnUnitTaskCompleted (completedTask: Task) (ctxt: AsyncActivation) = + assert completedTask.IsCompleted + if completedTask.IsCanceled then + let edi = ExceptionDispatchInfo.Capture(TaskCanceledException(completedTask)) + ctxt.econt edi + elif completedTask.IsFaulted then + let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception + ctxt.econt edi + else + ctxt.cont () - let continuation (completedTask: Task) : unit = + // Helper to attach continuation to the given task, which is assumed not to be completed. + // When the task completes the continuation will be run synchronously on the thread + // completing the task. This will install a new trampoline on that thread and continue the + // execution of the async there. + [] + let AttachContinuationToTask (task: Task<'T>) (ctxt: AsyncActivation<'T>) = + task.ContinueWith(Action>(fun completedTask -> ctxt.trampolineHolder.ExecuteWithTrampoline (fun () -> - if completedTask.IsCanceled then - let edi = ExceptionDispatchInfo.Capture(TaskCanceledException(completedTask)) - ctxt.econt edi - elif completedTask.IsFaulted then - let edi = ExceptionDispatchInfo.RestoreOrCapture completedTask.Exception - ctxt.econt edi - else - ctxt.cont ()) |> unfake - - if task.IsCompleted then - continuation task |> fake - else - task.ContinueWith(Action(continuation), TaskContinuationOptions.ExecuteSynchronously) - |> ignore |> fake + OnTaskCompleted completedTask ctxt) + |> unfake), TaskContinuationOptions.ExecuteSynchronously) + |> ignore + |> fake + + // Helper to attach continuation to the given task, which is assumed not to be completed + // When the task completes the continuation will be run synchronously on the thread + // completing the task. This will install a new trampoline on that thread and continue the + // execution of the async there. + [] + let AttachContinuationToUnitTask (task: Task) (ctxt: AsyncActivation) = + task.ContinueWith(Action(fun completedTask -> + ctxt.trampolineHolder.ExecuteWithTrampoline (fun () -> + OnUnitTaskCompleted completedTask ctxt) + |> unfake), TaskContinuationOptions.ExecuteSynchronously) + |> ignore + |> fake [] type AsyncIAsyncResult<'T>(callback: System.AsyncCallback, state:obj) = @@ -1693,15 +1707,15 @@ namespace Microsoft.FSharp.Control static member AwaitTask (task:Task<'T>) : Async<'T> = if task.IsCompleted then - CreateProtectedAsync (fun ctxt -> taskContinueWith task ctxt) + MakeAsync (fun ctxt -> OnTaskCompleted task ctxt) else - CreateDelimitedUserCodeAsync (fun ctxt -> taskContinueWith task ctxt) + CreateDelimitedUserCodeAsync (fun ctxt -> AttachContinuationToTask task ctxt) static member AwaitTask (task:Task) : Async = if task.IsCompleted then - CreateProtectedAsync (fun ctxt -> taskContinueWithUnit task ctxt) + MakeAsync (fun ctxt -> OnUnitTaskCompleted task ctxt) else - CreateDelimitedUserCodeAsync (fun ctxt -> taskContinueWithUnit task ctxt) + CreateDelimitedUserCodeAsync (fun ctxt -> AttachContinuationToUnitTask task ctxt) module CommonExtensions = diff --git a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs index 689bcec6d1..6247da500b 100644 --- a/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs +++ b/tests/FSharp.Core.UnitTests/FSharp.Core/Microsoft.FSharp.Control/AsyncType.fs @@ -21,9 +21,12 @@ type AsyncType() = let ignoreSynchCtx f = f () + let waitASec (t:Task) = + let result = t.Wait(TimeSpan(hours=0,minutes=0,seconds=1)) + Assert.True(result, "Task did not finish after waiting for a second.") [] - member this.StartWithContinuations() = + member _.StartWithContinuations() = let whatToDo = ref Exit @@ -75,7 +78,7 @@ type AsyncType() = () [] - member this.AsyncRunSynchronouslyReusesThreadPoolThread() = + member _.AsyncRunSynchronouslyReusesThreadPoolThread() = let action = async { async { () } |> Async.RunSynchronously } let computation = [| for i in 1 .. 1000 -> action |] @@ -90,7 +93,7 @@ type AsyncType() = [] [] [] - member this.AsyncSleepCancellation1(sleepType) = + member _.AsyncSleepCancellation1(sleepType) = ignoreSynchCtx (fun () -> let computation = match sleepType with @@ -112,7 +115,7 @@ type AsyncType() = [] [] [] - member this.AsyncSleepCancellation2(sleepType) = + member _.AsyncSleepCancellation2(sleepType) = ignoreSynchCtx (fun () -> let computation = match sleepType with @@ -137,7 +140,7 @@ type AsyncType() = [] [] [] - member this.AsyncSleepThrowsOnNegativeDueTimes(sleepType) = + member _.AsyncSleepThrowsOnNegativeDueTimes(sleepType) = async { try do! match sleepType with @@ -150,7 +153,7 @@ type AsyncType() = } |> Async.RunSynchronously [] - member this.AsyncSleepInfinitely() = + member _.AsyncSleepInfinitely() = ignoreSynchCtx (fun () -> let computation = Async.Sleep(System.Threading.Timeout.Infinite) let result = TaskCompletionSource() @@ -164,27 +167,17 @@ type AsyncType() = Assert.AreEqual("Cancel", result) ) - member private this.WaitASec (t:Task) = - let result = t.Wait(TimeSpan(hours=0,minutes=0,seconds=1)) - Assert.True(result, "Task did not finish after waiting for a second.") - - [] - member this.CreateTask () = + member _.CreateTask () = let s = "Hello tasks!" let a = async { return s } -#if !NET46 - let t : Task = -#else - use t : Task = -#endif - Async.StartAsTask a - this.WaitASec t + use t : Task = Async.StartAsTask a + waitASec t Assert.True (t.IsCompleted) Assert.AreEqual(s, t.Result) [] - member this.StartAsTaskCancellation () = + member _.StartAsTaskCancellation () = let cts = new CancellationTokenSource() let mutable spinloop = true let doSpinloop () = while spinloop do () @@ -192,12 +185,7 @@ type AsyncType() = cts.CancelAfter (100) doSpinloop() } -#if !NET46 - let t : Task = -#else - use t : Task = -#endif - Async.StartAsTask(a, cancellationToken = cts.Token) + use t : Task = Async.StartAsTask(a, cancellationToken = cts.Token) // Should not finish, we don't eagerly mark the task done just because it's been signaled to cancel. try @@ -208,7 +196,7 @@ type AsyncType() = spinloop <- false try - this.WaitASec t + waitASec t with :? AggregateException as a -> match a.InnerException with | :? TaskCanceledException as t -> () @@ -216,7 +204,7 @@ type AsyncType() = Assert.True (t.IsCompleted, "Task is not completed") [] - member this.``AwaitTask ignores Async cancellation`` () = + member _.``AwaitTask ignores Async cancellation`` () = let cts = new CancellationTokenSource() let tcs = new TaskCompletionSource() let innerTcs = new TaskCompletionSource() @@ -233,7 +221,7 @@ type AsyncType() = innerTcs.SetResult () try - this.WaitASec tcs.Task + waitASec tcs.Task with :? AggregateException as a -> match a.InnerException with | :? TaskCanceledException -> () @@ -241,7 +229,7 @@ type AsyncType() = Assert.True (tcs.Task.IsCompleted, "Task is not completed") [] - member this.RunSynchronouslyCancellationWithDelayedResult () = + member _.RunSynchronouslyCancellationWithDelayedResult () = let cts = new CancellationTokenSource() let tcs = TaskCompletionSource() let _ = cts.Token.Register(fun () -> tcs.SetResult 42) @@ -260,45 +248,35 @@ type AsyncType() = Assert.True (cancelled, "Task is not cancelled") [] - member this.ExceptionPropagatesToTask () = + member _.ExceptionPropagatesToTask () = let a = async { do raise (Exception ()) } -#if !NET46 - let t = -#else - use t = -#endif - Async.StartAsTask a + use t = Async.StartAsTask a let mutable exceptionThrown = false try - this.WaitASec t + waitASec t with e -> exceptionThrown <- true Assert.True (t.IsFaulted) Assert.True(exceptionThrown) [] - member this.CancellationPropagatesToTask () = + member _.CancellationPropagatesToTask () = let a = async { while true do () } -#if !NET46 - let t = -#else - use t = -#endif - Async.StartAsTask a + use t = Async.StartAsTask a Async.CancelDefaultToken () let mutable exceptionThrown = false try - this.WaitASec t + waitASec t with e -> exceptionThrown <- true Assert.True (exceptionThrown) Assert.True(t.IsCanceled) [] - member this.CancellationPropagatesToGroup () = + member _.CancellationPropagatesToGroup () = let ewh = new ManualResetEvent(false) let cancelled = ref false let a = async { @@ -308,67 +286,47 @@ type AsyncType() = } let cts = new CancellationTokenSource() let token = cts.Token -#if !NET46 - let t = -#else - use t = -#endif - Async.StartAsTask(a, cancellationToken=token) + use t = Async.StartAsTask(a, cancellationToken=token) // printfn "%A" t.Status ewh.WaitOne() |> Assert.True cts.Cancel() // printfn "%A" t.Status let mutable exceptionThrown = false try - this.WaitASec t + waitASec t with e -> exceptionThrown <- true Assert.True (exceptionThrown) Assert.True(t.IsCanceled) Assert.True(!cancelled) [] - member this.CreateImmediateAsTask () = + member _.CreateImmediateAsTask () = let s = "Hello tasks!" let a = async { return s } -#if !NET46 - let t : Task = -#else - use t : Task = -#endif - Async.StartImmediateAsTask a - this.WaitASec t + use t : Task = Async.StartImmediateAsTask a + waitASec t Assert.True (t.IsCompleted) Assert.AreEqual(s, t.Result) [] - member this.StartImmediateAsTask () = + member _.StartImmediateAsTask () = let s = "Hello tasks!" let a = async { return s } -#if !NET46 - let t = -#else - use t = -#endif - Async.StartImmediateAsTask a - this.WaitASec t + use t = Async.StartImmediateAsTask a + waitASec t Assert.True (t.IsCompleted) Assert.AreEqual(s, t.Result) [] - member this.ExceptionPropagatesToImmediateTask () = + member _.ExceptionPropagatesToImmediateTask () = let a = async { do raise (Exception ()) } -#if !NET46 - let t = -#else - use t = -#endif - Async.StartImmediateAsTask a + use t = Async.StartImmediateAsTask a let mutable exceptionThrown = false try - this.WaitASec t + waitASec t with e -> exceptionThrown <- true Assert.True (t.IsFaulted) @@ -377,20 +335,15 @@ type AsyncType() = #if IGNORED [] [] - member this.CancellationPropagatesToImmediateTask () = + member _.CancellationPropagatesToImmediateTask () = let a = async { while true do () } -#if !NET46 - let t = -#else - use t = -#endif - Async.StartImmediateAsTask a + use t = Async.StartImmediateAsTask a Async.CancelDefaultToken () let mutable exceptionThrown = false try - this.WaitASec t + waitASec t with e -> exceptionThrown <- true Assert.True (exceptionThrown) Assert.True(t.IsCanceled) @@ -399,7 +352,7 @@ type AsyncType() = #if IGNORED [] [] - member this.CancellationPropagatesToGroupImmediate () = + member _.CancellationPropagatesToGroupImmediate () = let ewh = new ManualResetEvent(false) let cancelled = ref false let a = async { @@ -417,7 +370,7 @@ type AsyncType() = // printfn "%A" t.Status let mutable exceptionThrown = false try - this.WaitASec t + waitASec t with e -> exceptionThrown <- true Assert.True (exceptionThrown) Assert.True(t.IsCanceled) @@ -425,14 +378,9 @@ type AsyncType() = #endif [] - member this.TaskAsyncValue () = + member _.TaskAsyncValue () = let s = "Test" -#if !NET46 - let t = -#else - use t = -#endif - Task.Factory.StartNew(Func<_>(fun () -> s)) + use t = Task.Factory.StartNew(Func<_>(fun () -> s)) let a = async { let! s1 = Async.AwaitTask(t) return s = s1 @@ -440,7 +388,7 @@ type AsyncType() = Async.RunSynchronously(a) |> Assert.True [] - member this.AwaitTaskCancellation () = + member _.AwaitTaskCancellation () = let test() = async { let tcs = new System.Threading.Tasks.TaskCompletionSource() tcs.SetCanceled() @@ -453,7 +401,7 @@ type AsyncType() = Async.RunSynchronously(test()) |> Assert.True [] - member this.AwaitCompletedTask() = + member _.AwaitCompletedTask() = let test() = async { let threadIdBefore = Thread.CurrentThread.ManagedThreadId do! Async.AwaitTask Task.CompletedTask @@ -464,7 +412,7 @@ type AsyncType() = Async.RunSynchronously(test()) |> Assert.True [] - member this.AwaitTaskCancellationUntyped () = + member _.AwaitTaskCancellationUntyped () = let test() = async { let tcs = new System.Threading.Tasks.TaskCompletionSource() tcs.SetCanceled() @@ -477,13 +425,8 @@ type AsyncType() = Async.RunSynchronously(test()) |> Assert.True [] - member this.TaskAsyncValueException () = -#if !NET46 - let t = -#else - use t = -#endif - Task.Factory.StartNew(Func(fun () -> raise <| Exception())) + member _.TaskAsyncValueException () = + use t = Task.Factory.StartNew(Func(fun () -> raise <| Exception())) let a = async { try let! v = Async.AwaitTask(t) @@ -494,16 +437,11 @@ type AsyncType() = // test is flaky: https://github.com/dotnet/fsharp/issues/11586 //[] - member this.TaskAsyncValueCancellation () = + member _.TaskAsyncValueCancellation () = use ewh = new ManualResetEvent(false) let cts = new CancellationTokenSource() let token = cts.Token -#if !NET46 - let t : Task= -#else - use t : Task= -#endif - Task.Factory.StartNew(Func(fun () -> while not token.IsCancellationRequested do ()), token) + use t : Task = Task.Factory.StartNew(Func(fun () -> while not token.IsCancellationRequested do ()), token) let cancelled = ref true let a = async { @@ -521,14 +459,9 @@ type AsyncType() = ewh.WaitOne(10000) |> ignore [] - member this.NonGenericTaskAsyncValue () = + member _.NonGenericTaskAsyncValue () = let hasBeenCalled = ref false -#if !NET46 - let t = -#else - use t = -#endif - Task.Factory.StartNew(Action(fun () -> hasBeenCalled := true)) + use t = Task.Factory.StartNew(Action(fun () -> hasBeenCalled := true)) let a = async { do! Async.AwaitTask(t) return true @@ -537,13 +470,8 @@ type AsyncType() = (!hasBeenCalled && result) |> Assert.True [] - member this.NonGenericTaskAsyncValueException () = -#if !NET46 - let t = -#else - use t = -#endif - Task.Factory.StartNew(Action(fun () -> raise <| Exception())) + member _.NonGenericTaskAsyncValueException () = + use t = Task.Factory.StartNew(Action(fun () -> raise <| Exception())) let a = async { try let! v = Async.AwaitTask(t) @@ -553,16 +481,11 @@ type AsyncType() = Async.RunSynchronously(a) |> Assert.True [] - member this.NonGenericTaskAsyncValueCancellation () = + member _.NonGenericTaskAsyncValueCancellation () = use ewh = new ManualResetEvent(false) let cts = new CancellationTokenSource() let token = cts.Token -#if !NET46 - let t = -#else - use t = -#endif - Task.Factory.StartNew(Action(fun () -> while not token.IsCancellationRequested do ()), token) + use t = Task.Factory.StartNew(Action(fun () -> while not token.IsCancellationRequested do ()), token) let a = async { try @@ -579,7 +502,7 @@ type AsyncType() = ewh.WaitOne(10000) |> ignore [] - member this.CancellationExceptionThrown () = + member _.CancellationExceptionThrown () = use ewh = new ManualResetEventSlim(false) let cts = new CancellationTokenSource() let token = cts.Token @@ -594,3 +517,20 @@ type AsyncType() = cts.Cancel() ewh.Wait(10000) |> ignore Assert.False hasThrown + + [] + member _.NoStackOverflowOnRecursion() = + + let mutable hasThrown = false + let rec loop (x: int) = async { + do! Task.CompletedTask |> Async.AwaitTask + Console.WriteLine (if x = 10000 then failwith "finish" else x) + return! loop(x+1) + } + + try + Async.RunSynchronously (loop 0) + hasThrown <- false + with Failure "finish" -> + hasThrown <- true + Assert.True hasThrown