F # Async Эквивалент Task.ContinueWith - PullRequest
2 голосов
/ 21 марта 2019

Я реализовал атрибут [<Trace>] для некоторых наших более крупных решений .NET, который позволит легко добавлять настраиваемую аналитику к любым функциям / методам, которые считаются важными. Я использую Fody и MethodBoundaryAspect , чтобы перехватывать вход и выход каждой функции и записывать метрики. Это хорошо работает для синхронных функций, а для методов, которые возвращают Task, существует работоспособное решение с Task.ContinueWith, но для функций F # с асинхронным возвратом OnExit из MethodBoundaryAspect запускается, как только возвращается Async (скорее чем когда асинхронное выполнение фактически выполняется).

Чтобы получить правильные метрики для функций, возвращающих F # Async, я пытался найти эквивалентное решение для использования Task.ContinueWith, но самое близкое, что я мог придумать, - это создать новый Async, который связывает первый один, запускает функции захвата метрики, а затем возвращает исходный результат. Это еще более усложняется тем фактом, что возвращаемое значение F # Async, которое я перехватываю, представляется только как obj, и после этого мне нужно все делать рефлексивно, поскольку не существует неуниверсальной версии Async, как с Task, который я могу использовать, не зная точного типа возврата.

Мое лучшее решение на данный момент выглядит примерно так:

open System
open System.Diagnostics
open FSharp.Reflection
open MethodBoundaryAspect.Fody.Attributes

[<AllowNullLiteral>]
[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Property, AllowMultiple = false)>]
type TraceAttribute () =
    inherit OnMethodBoundaryAspect()

    let traceEvent (args: MethodExecutionArgs) (timestamp: int64) =
        // Capture metrics here
        ()

    override __.OnEntry (args) =
        Stopwatch.GetTimestamp() |> traceEvent args

    override __.OnExit (args) =
        let exit () = Stopwatch.GetTimestamp() |> traceEvent args
        match args.ReturnValue with
        | :? System.Threading.Tasks.Task as task ->
            task.ContinueWith(fun _ -> exit()) |> ignore             
        | other -> // Here's where I could use some help
            let clrType = other.GetType()
            if clrType.IsGenericType && clrType.GetGenericTypeDefinition() = typedefof<Async<_>> then
                // If the return type is an F# Async, replace it with a new Async that calls exit after the original return value is computed
                let returnType = clrType.GetGenericArguments().[0]
                let functionType = FSharpType.MakeFunctionType(returnType, typedefof<Async<_>>.MakeGenericType([| returnType |]))
                let f = FSharpValue.MakeFunction(functionType, (fun _ -> exit(); other))
                let result = typeof<AsyncBuilder>.GetMethod("Bind").MakeGenericMethod([|returnType; returnType|]).Invoke(async, [|other; f|]) 
                args.ReturnValue <- result
            else
                exit()

К сожалению, это решение не только довольно грязное, но я считаю, что рефлексивная конструкция асинхронных вычислений добавляет нетривиальные издержки, особенно когда я пытаюсь отследить функции, которые вызываются в цикле или имеют глубоко вложенные асинхронные вызовы. Есть ли лучший способ достичь того же результата при запуске данной функции сразу после фактической оценки асинхронных вычислений?

Ответы [ 2 ]

1 голос
/ 22 марта 2019

Что-то вроде этого, вероятно, вам нужно:

let traceAsync (a:Async<_>) = async {
    trace() // trace start of async
    let! r = a
    trace() // trace end of async
    return r
}

Учтите, что когда функция возвращает асинхронность, это не означает, что асинхронность началась.Асинхронизация больше похожа на функцию, она может быть вызвана несколько раз или вообще не вызываться.Это означает, что вам нужно проверить, является ли возвращаемое значение асинхронным также в вашем методе OnEntry.

0 голосов
/ 22 марта 2019

Следуя совету @AMieres, я смог обновить свой метод OnExit, чтобы корректно отслеживать асинхронное выполнение без особых затрат. Я думаю, что основная часть проблемы заключалась в использовании того же экземпляра AsyncBuilder, что приводило к дополнительным вызовам асинхронных функций. Вот новое решение:

open System
open System.Diagnostics
open FSharp.Reflection
open MethodBoundaryAspect.Fody.Attributes

[<AllowNullLiteral>]
[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Property, AllowMultiple = false)>]
type TraceAttribute () =
    inherit OnMethodBoundaryAspect()
    static let AsyncTypeDef = typedefof<Async<_>>
    static let Tracer = typeof<TraceAttribute>
    static let AsyncTracer = Tracer.GetMethod("TraceAsync")

    let traceEvent (args: MethodExecutionArgs) (timestamp: int64) =
        // Capture metrics here
        ()

    member __.TraceAsync (asyncResult: Async<_>) trace =
        async {
            let! result = asyncResult
            trace()
            return result
        }

    override __.OnEntry (args) =
        Stopwatch.GetTimestamp() |> traceEvent args

    override __.OnExit (args) =
        let exit () = Stopwatch.GetTimestamp() |> traceEvent args
        match args.ReturnValue with
        | :? System.Threading.Tasks.Task as task ->
            task.ContinueWith(fun _ -> exit()) |> ignore             
        | other -> 
            let clrType = other.GetType()
            if clrType.IsGenericType && clrType.GetGenericTypeDefinition() = AsyncTypeDef then
                let generics = clrType.GetGenericArguments()
                let result = AsyncTracer.MakeGenericMethod(generics).Invoke(this, [| other; exit |])
                args.ReturnValue <- result
            else
                exit()

Это, кажется, правильно отслеживает асинхронные функции со значительно меньшими издержками. Я действительно хотел отследить общее время с момента вызова функции, а не с момента фактического запуска асинхронного преобразования, поэтому я оставил свою реализацию OnEntry такой же.

...