Асинхронный эквивалент F# для Task.ContinueWith

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

Чтобы получить правильные метрики для функций F#, возвращающих асинхронный режим, я пытался придумать решение, эквивалентное использованию Task.ContinueWith, но самое близкое, что я мог придумать, это создать новый асинхронный объект, который связывает первый, запускает метрику -захват функций, а затем возвращает исходный результат. Это еще более усложняется тем фактом, что возвращаемое значение 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()

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


person Aaron M. Eshbach    schedule 21.03.2019    source источник


Ответы (2)


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

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

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

person AMieres    schedule 21.03.2019
comment
Спасибо, это навело меня на правильный путь. На самом деле я хотел зафиксировать все время с момента вызова функции до завершения асинхронного выполнения, поэтому я оставил поведение OnEntry таким, каким оно было, но использование такой функции и ее рефлективный вызов в моем OnExit определенно было улучшением. - person Aaron M. Eshbach; 22.03.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 без изменений.

person Aaron M. Eshbach    schedule 22.03.2019