如何在F#中的异步工作流中重新使用?

Cha*_*ari 11 f# asynchronous exception-handling f#-async

在记录异常之后,我需要重新执行异步块时发生的异常.

当我执行以下操作时,编译器认为我没有在处理程序中调用reraise函数.我究竟做错了什么?

let executeAsync context = async {
    traceContext.Properties.Add("CorrelationId", context.CorrelationId)
    try
        do! runAsync context
        return None
    with
        | e when isCriticalException(e) ->
            logCriticalException e
            reraise()
        | e ->
            logException e
            return Some(e)
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*sen 13

粗!我认为这是不可能的,因为重新加载对应于从堆栈顶部抓取异常的特殊IL指令,但异步表达式被编译成连续的链的方式,我不认为语义成立!

出于同样的原因,以下内容也不会编译:

try
    (null:string).ToString()
with e ->
    (fun () -> reraise())()
Run Code Online (Sandbox Code Playgroud)

在这些情况下,我需要在实际with正文之外处理异常,并且想要模拟reraise(即保留异常的堆栈跟踪),我使用这个解决方案,所以你的代码总是如下:

let inline reraisePreserveStackTrace (e:Exception) =
    let remoteStackTraceString = typeof<exn>.GetField("_remoteStackTraceString", BindingFlags.Instance ||| BindingFlags.NonPublic);
    remoteStackTraceString.SetValue(e, e.StackTrace + Environment.NewLine);
    raise e

let executeAsync context = async {
    traceContext.Properties.Add("CorrelationId", context.CorrelationId)
    try
        do! runAsync context
        return None
    with
        | e when isCriticalException(e) ->
            logCriticalException e
            reraisePreserveStackTrace e
        | e ->
            logException e
            return Some(e)
}
Run Code Online (Sandbox Code Playgroud)

更新: .NET 4.5引入了ExceptionDispatchInfo,可以更清晰地实现reraisePreserveStackTrace上述功能.

  • @DaxFohl可以使用`ExceptionDispatchInfo`提供更新的答案吗? (2认同)
  • 我已将其作为F#建议提交,能否请您赞成?https://github.com/fsharp/fslang-suggestions/issues/660 (2认同)

Jam*_*aix 6

正如评论和答案更新中提到的,您可以将其用作ExceptionDispatchInfo解决方法。

open System.Runtime.ExceptionServices

let inline reraiseAnywhere<'a> (e: exn) : 'a =
    ExceptionDispatchInfo.Capture(e).Throw()
    Unchecked.defaultof<'a>
Run Code Online (Sandbox Code Playgroud)

用法:

async {
    try
         do! someAsyncThing ()
         return "Success"
    with
    | e ->
        if errorIsOk e then return "Handled error"
        else return (reraiseAnyWhere e)
}
Run Code Online (Sandbox Code Playgroud)

这是我用单元测试创​​建的 GitHub Gist,以检查其行为。

  • 在示例中,异步块的类型为“Async&lt;string&gt;”,因此每个“return”表达式必须返回一个“string”。因此,需要将 `reraiseAnywhere` 键入为返回 `'a`,即使它实际上从未返回任何值。 (2认同)

Joh*_*mer 5

我在不同的背景下遇到了类似的问题,但归结为这一点。

异常不能被抛出到不同的线程上 - 调用reraise()需要一个异常处理程序在某种意义上运行在代码中原始异步块的“上方”。

let runAsync context = async {return ()}
let isCriticalException e = true
let logCriticalException e = ()
let logException e = ()
let executeAsync context = 
    async {
            do! runAsync context
            return None
}

let run = 
    match executeAsync 5 |> Async.Catch |> Async.RunSynchronously with
    |Choice1Of2(t) -> 
        printfn "%A" t
        None
    |Choice2Of2(exn) ->  
            match exn with
            | e when isCriticalException(e) ->
                logCriticalException e
                raise (new System.Exception("See inner exception",e)) //stack trace will be lost at this point if the exn is not wrapped
            | e ->
                logException e
                Some(e)
Run Code Online (Sandbox Code Playgroud)

请注意,我们仍然无法使用重新引发,因为我们现在正在不同的线程上调用,因此我们将异常包装在另一个线程中