Question

Is it possible to make PostAndAsyncReply return immediately when its MailboxProcessor is disposed (or otherwise stopped)? Or is there some "pattern" / best practice on how to safely use PostAndReply methods without creating deadlocks?

Right now I have the problem that PostAndAsyncReply never returns when the MailboxProcessor has been disposed. Using the timeout parameter isn't an option since I cannot wait (besides that choosing reasonable timeouts is quite hard or impossible anyway since it depends on way too many factors).

  [<Test>]
  let ``waiting for a reply from a disposed agent``() =
    use server = MailboxProcessor.Start(fun inbox -> async {
      ()
    })

    (server :> System.IDisposable).Dispose()

    server.PostAndReply (fun reply -> reply) // <- deadlock
    |> ignore)

edit: Most examples with MailboxProcessors I have seen (incl. the ones on MSDN) don't even mind disposing the MailboxProcessors. And the MSDN doesn't explain how MailboxProcessors react when being disposed. Is it unnecessary to dispose them?

Was it helpful?

Solution

There is no built-in support for the cancellation of pending PostAndReply calls, but you can implement this. To handle for disposal, you can wrap the body of the mailbox in try .. finally. In the finally block, you can somehow signal that the mailbox processor has stopped. The following uses cancellation token source for this:

let disposed = new System.Threading.CancellationTokenSource()
let server = MailboxProcessor<AsyncReplyChannel<obj>>.Start(fun inbox -> 
  async { 
    try 
      // The normal body of the mailbox processor goes here
      do! Async.Sleep(1000)
      printfn "done"
    finally 
      // Cancel all pending calls post and reply
      disposed.Cancel()
   })

// Dispose the mailbox processor    
(server :> System.IDisposable).Dispose()

// When sending message, we use 'StartAsTask' and set a cancellation token,
// so that the work is stopped when the mailbox processor finishes
let wait = server.PostAndAsyncReply(fun reply -> reply)
let task = Async.StartAsTask(wait, cancellationToken = disposed.Token)
printfn "%A" task.Result

There are two things to keep in mind:

  • If the mailbox processor is running some long-running work (like sleeping above), then the disposal will happen after this is completed (this is because of the nature of async workflows - they do not forcefully cancel computations)

  • I had to use StartAsTask instead of RunSynchronously to start the task at the end. For some reason (not quite sure), using RunSynchronously does not seem to cancel the computation.

You could easily wrap this in some class that uses MailboxProcessor internally, exposes similar interface and adds this functionality - but that's a bit too much for a single answer!

To answer your question regarding Dispose, I'm not entirely sure what the behaviour is, but disposing the mailbox processor disposes an internal AutoResetEvent (see the source code) that is used to signal that a message has arrived. I suppose this just means that the mailbox processor will not accept any further messages.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top