Skip to content

MailboxProcessor.PostAnd(Async)Reply never returns #6285

Open
@stmax82

Description

@stmax82

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

Area-AsyncAsync supportBugImpact-Medium(Internal MS Team use only) Describes an issue with moderate impact on existing code.

Type

Projects

Status

New

Relationships

None yet

Development

No branches or pull requests

Issue actions