Compartir vía


Control de errores con Durable Functions (Azure Functions)

Las orquestaciones de Durable Functions se implementan en código y pueden utilizar las características de control de errores integradas en el lenguaje de programación. Realmente no hay ningún concepto nuevo que deba conocer para agregar el control de errores y la compensación en las orquestaciones. No obstante, hay algunos comportamientos que deben tenerse en cuenta.

Nota:

La versión 4 del modelo de programación de Node.js para Azure Functions está disponible de forma general. El nuevo modelo v4 está diseñado para que los desarrolladores de JavaScript y TypeScript tengan una experiencia más flexible e intuitiva. Obtenga más información sobre las diferencias entre v3 y v4 en la guía de migración.

En los siguientes fragmentos de código, JavaScript (PM4) indica el modelo de programación V4, la nueva experiencia.

Errores en las funciones de actividad y en las suborquestaciones

En Durable Functions, las excepciones no controladas generadas en las funciones de actividad o en las suborquestaciones se devuelven a la función de orquestador utilizando tipos de excepción estandarizados.

Por ejemplo, considere la siguiente función de orquestador que realiza una transferencia de fondos entre dos cuentas:

En las funciones duraderas de C# en modo de proceso, las excepciones no controladas se lanzan como FunctionFailedException.

El mensaje de excepción normalmente identifica qué funciones de actividad o sub orquestaciones provocaron el error. Para acceder a información más detallada sobre el error, inspeccione la propiedad InnerException.

[FunctionName("TransferFunds")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var transferDetails = context.GetInput<TransferOperation>();

    await context.CallActivityAsync("DebitAccount",
        new
        {
            Account = transferDetails.SourceAccount,
            Amount = transferDetails.Amount
        });

    try
    {
        await context.CallActivityAsync("CreditAccount",
            new
            {
                Account = transferDetails.DestinationAccount,
                Amount = transferDetails.Amount
            });
    }
    catch (FunctionFailedException)
    {
        // Refund the source account.
        // Another try/catch could be used here based on the needs of the application.
        await context.CallActivityAsync("CreditAccount",
            new
            {
                Account = transferDetails.SourceAccount,
                Amount = transferDetails.Amount
            });
    }
}

Nota:

Los ejemplos de C# anteriores corresponden a Durable Functions 2.x. En el caso de Durable Functions 1.x, debe usar DurableOrchestrationContext en lugar de IDurableOrchestrationContext. Para obtener más información sobre las diferencias entre versiones, vea el artículo Versiones de Durable Functions.

Si se produce un error en la primera llamada a la función CreditAccount, la función de orquestador lo compensa al devolver los fondos a la cuenta de origen.

Errores en las funciones de entidad

El comportamiento de control de excepciones para las funciones de entidad difiere en función del modelo de hospedaje de Durable Functions:

En Durable Functions, usando C# in-process, los tipos de excepción originales generados por las funciones de entidad se devuelven directamente al orquestador.

[FunctionName("Function1")]
public static async Task<string> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    try
    {
        var entityId = new EntityId(nameof(Counter), "myCounter");
        await context.CallEntityAsync(entityId, "Add", 1);
    }
    catch (Exception ex)
    {
        // The exception type will be InvalidOperationException with the message "this is an entity exception".
    }
    return string.Empty;
}

[FunctionName("Counter")]
public static void Counter([EntityTrigger] IDurableEntityContext ctx)
{
    switch (ctx.OperationName.ToLowerInvariant())
    {
        case "add":
            throw new InvalidOperationException("this is an entity exception");
        case "get":
            ctx.Return(ctx.GetState<int>());
            break;
    }
}

Reintento automático en caso de error

Al llamar a funciones de actividad o funciones de suborquestación, puede especificar una directiva de reintentos automáticos. En el ejemplo siguiente se intenta llamar a una función hasta 3 veces y se espera 5 segundos entre cada reintento:

[FunctionName("TimerOrchestratorWithRetry")]
public static async Task Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var retryOptions = new RetryOptions(
        firstRetryInterval: TimeSpan.FromSeconds(5),
        maxNumberOfAttempts: 3);

    await context.CallActivityWithRetryAsync("FlakyFunction", retryOptions, null);

    // ...
}

Nota:

Los ejemplos de C# anteriores corresponden a Durable Functions 2.x. En el caso de Durable Functions 1.x, debe usar DurableOrchestrationContext en lugar de IDurableOrchestrationContext. Para obtener más información sobre las diferencias entre versiones, vea el artículo Versiones de Durable Functions.

La llamada a la función de actividad en el ejemplo anterior toma un parámetro para configurar una directiva de reintento automático. Existen varias opciones para personalizar la directiva de reintentos automáticos:

  • Max number of attempts (Número máximo de intentos): número máximo de intentos. Si se establece en 1, no habrá ningún reintento.
  • First retry interval (Intervalo para el primer reintento): cantidad de tiempo de espera antes del primer reintento.
  • Backoff coefficient (Coeficiente de retroceso): coeficiente que se usa para determinar la tasa de incremento del retroceso. De manera predeterminada, su valor es 1.
  • Max retry interval (Intervalo de reintento máximo): cantidad máxima de tiempo de espera entre reintentos.
  • Retry timeout (Tiempo de espera de reintento): cantidad máxima de tiempo durante el que realizar reintentos. El comportamiento predeterminado es realizar reintentos de manera indefinida.

Controladores de reintento personalizados

Cuando se usa .NET o Java, también se tiene la opción de implementar controladores de reintento en el código. Esto resulta útil cuando las políticas de reintento declarativas no tienen suficiente expresividad. Para los lenguajes que no admiten controladores de reintentos personalizados, todavía tienes la opción de implementar políticas de reintentos mediante bucles, manejo de excepciones y temporizadores para inyectar retrasos entre reintentos.

RetryOptions retryOptions = new RetryOptions(
    firstRetryInterval: TimeSpan.FromSeconds(5),
    maxNumberOfAttempts: int.MaxValue)
    {
        Handle = exception =>
        {
            // True to handle and try again, false to not handle and throw.
            if (exception is TaskFailedException failure)
            {
                // Exceptions from TaskActivities are always this type. Inspect the
                // inner Exception to get more details.
            }

            return false;
        };
    }

await ctx.CallActivityWithRetryAsync("FlakeyActivity", retryOptions, null);

Tiempos de espera de función

Es posible que desee abandonar una llamada de función dentro de una función de orquestador si tarda demasiado tiempo en completarse. La forma adecuada de hacerlo hoy en día es creando un temporizador durable con un selector de tarea "cualquiera", como en el siguiente ejemplo:

[FunctionName("TimerOrchestrator")]
public static async Task<bool> Run([OrchestrationTrigger] IDurableOrchestrationContext context)
{
    TimeSpan timeout = TimeSpan.FromSeconds(30);
    DateTime deadline = context.CurrentUtcDateTime.Add(timeout);

    using (var cts = new CancellationTokenSource())
    {
        Task activityTask = context.CallActivityAsync("FlakyFunction");
        Task timeoutTask = context.CreateTimer(deadline, cts.Token);

        Task winner = await Task.WhenAny(activityTask, timeoutTask);
        if (winner == activityTask)
        {
            // success case
            cts.Cancel();
            return true;
        }
        else
        {
            // timeout case
            return false;
        }
    }
}

Nota:

Los ejemplos de C# anteriores corresponden a Durable Functions 2.x. En el caso de Durable Functions 1.x, debe usar DurableOrchestrationContext en lugar de IDurableOrchestrationContext. Para obtener más información sobre las diferencias entre versiones, vea el artículo Versiones de Durable Functions.

Nota:

Este mecanismo no finaliza realmente la ejecución de la función de actividad en curso. En su lugar, simplemente permite que la función de orquestador pase por alto el resultado y continúe. Consulte la documentación sobre temporizadores para más información.

Excepciones no controladas

Si se produce un error en una función de orquestador con una excepción no controlada, se registran los detalles de la excepción y la instancia se completa con el estado Failed.

Incluir propiedades de excepción personalizadas para FailureDetails (.NET Isolated)

Al ejecutar flujos de trabajo de Durable Task en el modelo aislado de .NET, los errores de tarea se serializan automáticamente en un objeto FailureDetails. De forma predeterminada, este objeto incluye campos estándar como:

  • ErrorType: el nombre del tipo de excepción
  • Mensaje: el mensaje de excepción
  • StackTrace — la traza de pila serializada
  • InnerFailure: un objeto FailureDetails anidado para excepciones internas recursivas

A partir de Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0, puede ampliar este comportamiento implementando un IExceptionPropertiesProvider (definido en el paquete Microsoft.DurableTask.Worker a partir de v1.16.1). Este proveedor define qué tipos de excepción y cuál de sus propiedades deben incluirse en el diccionario FailureDetails.Properties.

Nota:

  • Esta característica está disponible solo en .NET Aislado. La compatibilidad con Java se agregará en una versión futura.
  • Asegúrese de que usa Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0 o posterior.
  • Asegúrese de que usa Microsoft.DurableTask.Worker v1.16.1 o posterior.

Implementar un proveedor de propiedades de excepción

Implemente un IExceptionPropertiesProvider personalizado para extraer y devolver propiedades seleccionadas para las excepciones que le interesan. El diccionario devuelto se serializará en el campo Propiedades de FailureDetails cuando se produzca un tipo de excepción coincidente.

using Microsoft.DurableTask.Worker;

public class CustomExceptionPropertiesProvider : IExceptionPropertiesProvider
{
    public IDictionary<string, object?>? GetExceptionProperties(Exception exception)
    {
        return exception switch
        {
            ArgumentOutOfRangeException e => new Dictionary<string, object?>
            {
                ["ParamName"] = e.ParamName,
                ["ActualValue"] = e.ActualValue
            },
            InvalidOperationException e => new Dictionary<string, object?>
            {
                ["CustomHint"] = "Invalid operation occurred",
                ["TimestampUtc"] = DateTime.UtcNow
            },
            _ => null // Other exception types not handled
        };
    }
}

Registrar el proveedor

Registra IExceptionPropertiesProvider personalizado en tu host de trabajo aislado de .NET, normalmente en Program.cs:

using Microsoft.DurableTask.Worker;
using Microsoft.Extensions.DependencyInjection;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults(builder =>
    {
        // Register custom exception properties provider
        builder.Services.AddSingleton<IExceptionPropertiesProvider, CustomExceptionPropertiesProvider>();
    })
    .Build();

host.Run();

Una vez registrada, cualquier excepción que coincida con uno de los tipos controlado incluirá automáticamente las propiedades configuradas en sus FailureDetails.

Salida de muestra de FailureDetails

Cuando se produce una excepción que coincide con la configuración del proveedor, la orquestación recibe una estructura FailureDetails serializada de la siguiente manera:

{
  "errorType": "TaskFailedException",
  "message": "Activity failed with an exception.",
  "stackTrace": "...",
  "innerFailure": {
    "errorType": "ArgumentOutOfRangeException",
    "message": "Specified argument was out of range.",
    "properties": {
      "ParamName": "count",
      "ActualValue": 42
    }
  }
}

Pasos siguientes