3

I'm trying to get started working with agents in F# via the MailboxProcessor<'Msg> class, and I quickly realized I had no proper handling for exceptions. In a Haskellian world, there wouldn't be any exceptions, and so the proper way to handle issues would be to simply provide them as a response case; so an agent could reply with something like:

type AgentResponse =
    | HandledData of string
    | InvalidData of string

One could then call the agent's .PostAndReply method and get an InvalidData containing a message indicating why the data is invalid. However, this isn't Haskell, and sometimes exceptions happen. So if I do this:

let agent =
    new MailboxProcessor<string * AsyncReplyChannel<AgentResponse>>(fun inbox ->
        async {
            while true do
                let! (msg, replyChannel) = inbox.Receive()
                if msg = "die" then failwith "Unknown exception encountered!"
                else replyChannel.Reply(HandledData(msg))
        })

agent.Start()
agent.PostAndReply (fun rc -> "die", rc)
agent.PostAndReply (fun rc -> "Test", rc)

The second call to agent.PostAndReply blocks indefinitely. When not using AsyncReplyChannel and therefore just calling agent.Post, the calls don't block, but once the agent encounters an exception, new messages just stay in the queue. Restarting the agent in either case appears impossible, as the agent.Start function returns an InvalidOperationException when called again, and the natural way to handle this seems to be to create a new agent with a clean state, but then all queued messages are lost.

Beyond just wrapping the entire body of an agent in try..with, is there any good way to get an agent to continue running after an exception? Alternatively, has a "standard" way of dealing with this been established that someone can point me to?

Amazingant
  • 531
  • 4
  • 13
  • I think the whole idea of a `MailboxProcessor` (agent) is that it dies on failure. Maybe you could use another agent as a queue, and then use `TryPostAndReply` where the queue agent spins up a new agent when there is an error? If you're needing high performance, I'm thinking `try...catch` is probably the best way to go. – N_A Jan 02 '15 at 17:15
  • 1
    @mydogisbox Just catching everything seems like a less-than-optimal idea, but I agree it also seems like the best way to go. The issue I'm looking to avoid is having to keeping two copies of the queued messages; e.g., if I wrote a mailer daemon in F#. The daemon shouldn't "lose" queued emails just because the agent hit a network exception I hadn't thought to catch, so it would need to keep the messages elsewhere as well as in the queue. Feels like it defeats the purpose of the agent having a queue if I need a second copy of everything in said queue. – Amazingant Jan 02 '15 at 18:19
  • I agree. I wonder how the erlang agent model handles this issue. – N_A Jan 02 '15 at 18:23

2 Answers2

3

You do have exceptions in Haskell : try Data.List.head [] in ghci....

Unfortunately, lack of dependent types means, in Haskell or in F#, that we can write type correct code which has no computational meaning.

Practically, wrapping the with a try ... with block is not a bad idea for dealing with exception. You dont need to wrap the entire body, just the non-pure part of your code though.

Then classically, you yield back a value made with the appropriate constructor.

nicolas
  • 9,549
  • 3
  • 39
  • 83
  • 1
    "just the non-pure part of your code" in .NET, wouldn't that be all of it? :) Waiting and wishing the pure keyword would happen: https://fslang.uservoice.com/forums/245727-f-language/suggestions/5670335-pure-functions-pure-keyword – Amazingant Jan 02 '15 at 16:02
  • Perhaps, but unenforced pure code at that; at that point, why have languages like Haskell at all? Besides it being rather difficult to determine which objects in the BCL (and third-party libs) are safe to use in a pure manner. – Amazingant Jan 02 '15 at 16:26
  • @nicolas : A rather unfair comparison, since C doesn't have exceptions (ignoring floating-point signals), whereas 90% of BCL methods may throw. – ildjarn Jan 02 '15 at 19:35
2

One possible option is a helper type similar to the HandlingMailbox defined by Tomas Petricek over at this question, which runs the body of the agent repeatedly:

type ResilientMailbox<'T> private(f:ResilientMailbox<'T> -> Async<unit>) as self =
    let event = Event<_>()
    let inbox = new MailboxProcessor<_>(fun _inbox ->
        let rec loop() = async {
            try
                return! f self
            with e ->
                event.Trigger(e)
                return! loop()
            }
        loop())
    member __.OnError = event.Publish
    member __.Start() = inbox.Start()
    member __.Receive() = inbox.Receive()
    member __.Post(v:'T) = inbox.Post(v)
    static member Start(f) =
        let mbox = new ResilientMailbox<_>(f)
        mbox.Start()
        mbox

This can be started and run like a normal MailboxProcessor, but will re-run itself if the provided agent body throws an exception.

Edit: Change the inner MailboxProcessor to use a recursive function versus a while true do.. block; the previous code would not stop running when the target function returned normally.

Community
  • 1
  • 1
Amazingant
  • 531
  • 4
  • 13