Open
Description
Below are 9 unit tests, 2 green / 7 red.
Situation
The red tests show cases where the MailboxProcessor.PostAnd(Async)Reply calls get stuck for eternity due to different reasons. I think things should never get stuck forever.
This might be related to case #4448, which was fixed a while ago, but there seem to be more cases with similar effects.
Workaround
Minimally set a cancellation token, e.g. cancellationToken=CancellationToken.None
on the mailbox processor (which is intended only to govern the asynchronous computation running in the mailbox, and not the request-reply to fetch information). This changes the behaviour of MailboxProcessor.PostAndAsyncReply
and friends to respect cancellation
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)), cancellationToken=CancellationToken.None)
Repro
Tested with FSharp.Core 4.6.2.
namespace UnitTestProject1
open System
open System.Threading
open Microsoft.VisualStudio.TestTools.UnitTesting
[<TestClass>]
type AsyncCancellationTests () =
// This test is GREEN because both MailboxProcess.Start and PostAndAsyncReply have a cancellation token
// As expected - it throws an OperationCanceledException
[<TestMethod; Timeout(2000)>]
member this.Test1() =
use cancel = new CancellationTokenSource(1000)
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)), cancellationToken=cancel.Token)
try
Async.RunSynchronously(async {
let! reply = mbox.PostAndAsyncReply(id)
()
}, cancellationToken=cancel.Token)
with
| :? OperationCanceledException -> ()
// This test is RED because PostAndAsyncReply doesn't get cancelled even though its containing async gets cancelled
// Expected: it should throw an OperationCanceledException
[<TestMethod; Timeout(2000)>]
member this.Test2() =
use cancel = new CancellationTokenSource(1000)
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)))
try
Async.RunSynchronously(async {
let! reply = mbox.PostAndAsyncReply(id)
()
}, cancellationToken=cancel.Token)
with
| :? OperationCanceledException -> ()
// This test is fishy GREEN even though its equal to Test2 (RED), except that the MailboxProcessor now gets a CancellationToken.None
// As expected - it throws an OperationCanceledException
[<TestMethod; Timeout(2000)>]
member this.Test2_2() =
use cancel = new CancellationTokenSource(1000)
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)), cancellationToken=CancellationToken.None)
try
Async.RunSynchronously(async {
let! reply = mbox.PostAndAsyncReply(id)
()
}, cancellationToken=cancel.Token)
with
| :? OperationCanceledException -> ()
// This test is RED because PostAndAsyncReply doesn't get cancelled when its MailboxProcessor gets cancelled
// Expected: it should throw an OperationCanceledException
[<TestMethod; Timeout(2000)>]
member this.Test3() =
use cancel = new CancellationTokenSource(1000)
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)), cancellationToken=cancel.Token)
try
Async.RunSynchronously(async {
let! reply = mbox.PostAndAsyncReply(id)
()
})
with
| :? OperationCanceledException -> ()
// This test is RED because PostAndAsyncReply gets stuck on a MailboxProcessor after it has been cancelled
// Expected: it should throw an OperationCanceledException
[<TestMethod; Timeout(2000)>]
member this.Test4() =
use cancel = new CancellationTokenSource()
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)), cancellationToken=cancel.Token)
cancel.Cancel()
Thread.Sleep(1000)
try
Async.RunSynchronously(async {
let! reply = mbox.PostAndAsyncReply(id)
()
})
with
| :? OperationCanceledException -> ()
// This test is RED because PostAndAsyncReply gets stuck on a disposed MailboxProcessor.
// Expected: it should throw an ObjectDisposedException
[<TestMethod; Timeout(2000)>]
member this.Test5() =
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)))
(mbox :> IDisposable).Dispose()
try
Async.RunSynchronously(async {
let! reply = mbox.PostAndAsyncReply(id)
()
})
with
| :? ObjectDisposedException -> ()
// This test is RED because PostAndReply gets stuck on a disposed MailboxProcessor.
// Expected: it should throw an ObjectDisposedException
[<TestMethod; Timeout(2000)>]
member this.Test6() =
let mbox = MailboxProcessor.Start((fun inbox -> Async.Sleep(1000000)))
(mbox :> IDisposable).Dispose()
try
let reply = mbox.PostAndReply(id)
()
with
| :? ObjectDisposedException -> ()
// This test is RED because PostAndReply gets stuck on a MailboxProcessor that has already exited.
// Expected: it should throw some exception.. maybe ObjectDisposed or InvalidOperation?
[<TestMethod; Timeout(2000)>]
member this.Test7() =
let mbox = MailboxProcessor.Start((fun inbox -> async { () }))
Thread.Sleep(1000)
try
let reply = mbox.PostAndReply(id)
()
with
| :? ObjectDisposedException -> ()
// This test is RED because PostAndAsyncReply gets stuck on a MailboxProcessor that has already exited.
// Expected: it should throw some exception.. maybe ObjectDisposed or InvalidOperation?
[<TestMethod; Timeout(2000)>]
member this.Test8() =
let mbox = MailboxProcessor.Start((fun inbox -> async { () }))
Thread.Sleep(1000)
try
Async.RunSynchronously(async {
let! reply = mbox.PostAndAsyncReply(id)
()
})
with
| :? ObjectDisposedException -> ()
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
New