MailboxProcessor first loop can't run if program immediately fails

时间:2019-03-19 14:48:43

标签: asynchronous f# mailboxprocessor

I have a command running a SFTP check periodically and logging the result to a file.

let logPath = Path.Combine(config.["SharedFolder"],timestamp)
let sw = new StreamWriter(logPath,true)
//...
[<EntryPoint>]
let main argv = 
    try 
        sftpExample config.["SharedFolder"] config.["SFTPFolder"] 22 "usr" "pswd" |> ignore
    with 
    | ex -> 
        ex.Message |> printerAgent.Post
        printfn "%s" ex.Message // <- NOTICE THIS LINE
    sw.Close()
    sw.Dispose()
0  

It loops over a MailboxProcessor

let printerAgent = MailboxProcessor.Start(fun inbox-> 
    // the message processing function
    let rec messageLoop() = async{        
        // read a message
        let! msg = inbox.Receive()
        // process a message
        sw.WriteLine("{0}: {1}", DateTime.UtcNow.ToShortTimeString(), msg)
        printfn "%s" msg
        // loop to top
        return! messageLoop()  
        }
    // start the loop 
    messageLoop() 
    )

which is called to write the messages to the log

let sftpExample local host port username (password:string) =
    async {
        use client = new SftpClient(host, port, username, password)
        client.Connect()
        sprintf "Connected to %s\nroot dir list" host  |> printerAgent.Post
        do! downloadDir local client ""   
        sprintf "Done, disconnecting now" |> printerAgent.Post
        client.Disconnect()
    } |> Async.RunSynchronously

The file downloads are asynchronous, as well as the corresponding messages, but all appears to work well.

The problem is that - if, for some reasons, the sftp connection immediately fails, the MailboxProcessor has no time to log the exception message.

What I've tried to do - which is working indeed - was adding a printfn "%s" ex.Message before the end: I just wanted to know if someone envisions a better solution.

FYI, the full code is in this gist.

2 个答案:

答案 0 :(得分:3)

实际上,您要的是程序等待,直到MailboxProcessor在程序退出之前完成对所有消息队列的处理。您的printfn "%s" ex.Message似乎可以正常工作,但不能保证能正常工作:如果MailboxProcessor队列中有多个项目,则运行printfn函数的线程可能在MailboxProcessor的线程有时间通过​​之前完成。它的所有消息。

我建议的设计是将printerAgent的输入更改为DU,如下所示:

type printerAgentMsg =
    | Message of string
    | Shutdown

然后,当您希望打印机代理完成其消息的发送时,请在main函数中使用MailboxProcessor.PostAndReply(并注意文档中的用法示例)并向其发送Shutdown消息。请记住,MailboxProcessor消息已排队:在收到Shutdown消息时,它将已经遍历队列中的其余消息。因此,处理Shutdown消息所需要做的就是返回unit答复,而不必再次调用其循环。并且由于使用的是PostAndReply而不是PostAndReplyAsync,所以主函数将一直阻塞,直到MailboxProcessor完成所有工作为止。 (为避免永远被阻塞的机会,建议您在PostAndReply呼叫中将超时设置为10秒;默认超时为-1,表示永远等待。)

编辑:这是我的意思的示例(未经测试,使用后果自负)

type printerAgentMsg =
    | Message of string
    | Shutdown of AsyncReplyChannel<unit>

let printerAgent = MailboxProcessor.Start(fun inbox-> 
    // the message processing function
    let rec messageLoop() = async{        
        // read a message
        let! msg = inbox.Receive()
        // process a message
        match msg with
        | Message text ->
            sw.WriteLine("{0}: {1}", DateTime.UtcNow.ToShortTimeString(), text)
            printfn "%s" text
            // loop to top
            return! messageLoop()
        | Shutdown replyChannel ->
            replyChannel.Reply()
            // We do NOT do return! messageLoop() here
        }
    // start the loop 
    messageLoop() 
    )

let logPath = Path.Combine(config.["SharedFolder"],timestamp)
let sw = new StreamWriter(logPath,true)
//...
[<EntryPoint>]
let main argv = 
    try 
        sftpExample config.["SharedFolder"] config.["SFTPFolder"] 22 "usr" "pswd" |> ignore
    with 
    | ex -> 
        ex.Message |> Message |> printerAgent.Post
        printfn "%s" ex.Message // <- NOTICE THIS LINE
    printerAgent.PostAndReply( (fun replyChannel -> Shutdown replyChannel), 10000)  // Timeout = 10000 ms = 10 seconds
    sw.Close()
    sw.Dispose()

答案 1 :(得分:1)

最简单的解决方案是使用常规(同步)功能记录而不是MailboxProcessor,或者在主要功能的末尾使用一些记录框架和刷新记录器。如果您想继续使用printingAgent,则可以实现“同步”模式,如下所示:

type Msg =
    | Log of string
    | LogAndWait of string * AsyncReplyChannel<unit>

let printerAgent = MailboxProcessor.Start(fun inbox -> 
    let processLogMessage logMessage =
        sw.WriteLine("{0}: {1}", DateTime.UtcNow.ToShortTimeString(), logMessage)
        printfn "%s" logMessage
    let rec messageLoop() = async{        
        let! msg = inbox.Receive()
        match msg with 
        | Log logMessage ->
            processLogMessage logMessage
        | LogAndWait (logMessage, replyChannel) ->
            processLogMessage logMessage
            replyChannel.Reply()
        return! messageLoop()  
        }
    messageLoop() 
    )

然后您将异步使用

printerAgent.Post(Log "Message")

或同步

printerAgent.PostAndReply(fun channel -> LogAndWait("Message", channel))

在主功能中记录异常时,应使用同步替代。