6

I'm using an agent (MailboxProcessor) to do some stateful processing where a response is needed.

  • The caller posts a message using MailboxProcessor.PostAndAsyncReply
  • Within the agent, a response is given with AsyncReplyChannel.Reply

However, I've discovered by poking around the f# source code that the agent will not process the next message until the the response is delivered. This is a good thing in general. But in my case, it is more desirable for the agent to keep processing messages than to wait for response delivery.

Is it problematic to do something like this to deliver the response? (Or is there a better alternative?)

async { replyChannel.Reply response } |> Async.Start

I realize that this method does not guarantee that responses will be delivered in order. I'm okay with that.

Reference example

// agent code
let doWork data =
    async { ... ; return response }

let rec loop ( inbox : MailboxProcessor<_> ) =
    async {
        let! msg = inbox.Receive()
        match msg with
        | None ->
            return ()

        | Some ( data, replyChannel ) ->
            let! response = doWork data
            replyChannel.Reply response (* waits for delivery, vs below *)
            // async { replyChannel.Reply response } |> Async.Start
            return! loop inbox
    }

let agent =
    MailboxProcessor.Start(loop)

// caller code
async {
    let! response =
        agent.PostAndAsyncReply(fun replyChannel -> Some (data, replyChannel))
    ...
}
Kasey Speakman
  • 4,511
  • 2
  • 32
  • 41
  • 1
    What you're trying to do kind of defeats the point of `PostAndAsyncReply`, so why use it at all? Have the client pass in, say, an observable subject, and push your replies into it. – Fyodor Soikin Feb 08 '17 at 16:43
  • 1
    The synchronization cost has to be paid either way (with observable or with reply channel) to deliver the response. And the method used in the question adds no extra lines of code and introduces no new concepts. But if you can identify other trade-offs and maybe provide an example, it could make a great answer. – Kasey Speakman Feb 08 '17 at 16:56
  • 1
    You've identified the trade-off yourself: reply channel doesn't work out of sequence. – Fyodor Soikin Feb 08 '17 at 17:31
  • 1
    Let me see if I understand your objective correctly: 1. You want the agent to not wait for message delivery, so that it can continue to process messages while the previous message is in-transit 2. You want the caller to process the message synchronously? – N_A Feb 08 '17 at 17:51
  • 1
    @mydogisbox 1 Yes. 2 No. The caller is also operating in an Async which resumes when the response is provided. The "synchronization cost" mentioned in the previous comment has to do with the cost of coordinating threads under the covers. I.e. f# `AsyncReplyChannel` internally uses a `ManualResetEvent` to signal the result as being ready. So when `Reply` is called, it does not return until the response has been delivered. This effective makes the agent wait until the caller receives the response before processing the next message. – Kasey Speakman Feb 08 '17 at 18:20
  • Got it. Sounds like you want functionality which isn't compatible with the MailboxProcessor. Perhaps you could take the original code and write your own? – N_A Feb 08 '17 at 19:10
  • I wouldn't say it's incompatible. The way I mentioned in the question works. But I didn't know if that will cause other negative effects. I was hoping someone more familiar with `MailboxProcessor` would know the answer or a better alternative. – Kasey Speakman Feb 08 '17 at 19:14

1 Answers1

1

FSharp.Control.AsyncSeq puts a friendlier face on top of mailbox processor. Async Sequences are a bit easier to follow, however the default implement mapping parallel has the same issue as described, waiting for the prior element in the sequence to be mapped to retain the order.

So I made a new function taht is just the original AsyncSeq.mapAsyncParallel, modified so that it no longer is a true map, since it's unordered, but it does map everything and the lazy seq does progress as work completes.

Full Source for AsyncSeq.mapAsyncParallelUnordered

let mapAsyncParallelUnordered (f:'a -> Async<'b>) (s:AsyncSeq<'a>) : AsyncSeq<'b> = asyncSeq {
  use mb = MailboxProcessor.Start (fun _ -> async.Return())
  let! err =
    s 
    |> AsyncSeq.iterAsyncParallel (fun a -> async {
      let! b = f a
      mb.Post (Some b) })
    |> Async.map (fun _ -> mb.Post None)
    |> Async.StartChildAsTask
  yield! 
    AsyncSeq.replicateUntilNoneAsync (Task.chooseTask (err |> Task.taskFault) (async.Delay mb.Receive))
  }

Below is an example of how I use it in a tool that uses SSLlabs free and very slow api that can easily get overloaded. parallelProcessHost returns an lazy AsyncSeq that is generated by the webapi requests, So AsyncSeq.mapAsyncParallelUnordered AsyncSeq.toListAsync actually runs the requests and allows the console to printout the results as the come in, independent of the order sent.

Full Source

let! es = 
    hosts
    |> Seq.indexed
    |> AsyncSeq.ofSeq
    |> AsyncSeq.map parallelProcessHost
    |> AsyncSeq.mapAsyncParallelUnordered AsyncSeq.toListAsync
    |> AsyncSeq.indexed
    |> AsyncSeq.map (fun (i, tail) -> (consoleN "-- %d of %i --- %O --" (i+1L) totalHosts (DateTime.UtcNow - startTime)) :: tail )
    |> AsyncSeq.collect AsyncSeq.ofSeq
    |> AsyncSeq.map stdoutOrStatus //Write out to console
    |> AsyncSeq.fold (|||) ErrorStatus.Okay
jbtule
  • 31,383
  • 12
  • 95
  • 128
  • I appreciate the answer, upvoted. Was collecting statistics from different running agents about what they processed. It's why reply order doesn't really matter and didn't want to hold up processing of the agents with sync reply. Ended up using a different strategy anyway. – Kasey Speakman Mar 08 '22 at 16:30