Поделиться через


Обработка ошибок в управлении оркестрацией

Вы реализуете оркестрации Durable Functions в коде, поэтому используете встроенные в язык функции обработки ошибок. Обработка ошибок и компенсация не требуют новых концепций, но о некоторых поведениях оркестрации стоит знать.

Замечание

Общедоступна версия 4 модели программирования Node.js для функций Azure. Модель версии 4 предназначена для обеспечения более гибкого и интуитивно понятного интерфейса для разработчиков JavaScript и TypeScript. Дополнительные сведения о различиях между версиями 3 и 4 см. в руководстве по миграции.

В следующих фрагментах кода JavaScript (PM4) обозначает модель программирования версии 4, новый интерфейс.

Приложения, использующие облачные службы, должны обрабатывать сбои, и повторные попытки на стороне клиента являются важной частью разработки. Пакеты SDK для устойчивых задач включают поддержку обработки ошибок, повторных попыток и времени ожидания для создания надежных рабочих процессов.

Ошибки в функциях активности и подоркестрациях

В Durable Functions необработанные исключения, создаваемые в функциях действий или вложенных оркестрациях, маршалируются обратно в функцию оркестратора с использованием стандартных типов исключений.

Следующая функция оркестратора передает средства между двумя счетами:

Изолированная рабочая модель

В изолированной среде Durable Functions C# необработанные исключения проявляются как TaskFailedException.

Сообщение об исключении обычно определяет, какие функции активности или под-оркестрации вызвали сбой. Чтобы получить доступ к более подробным сведениям об ошибке, проверьте свойство FailureDetails .

[FunctionName("TransferFunds")]
public static async Task Run(
    [OrchestrationTrigger] TaskOrchestrationContext context, TransferOperation transferDetails)
{
    await context.CallActivityAsync("DebitAccount",
        new
        {
            Account = transferDetails.SourceAccount,
            Amount = transferDetails.Amount
        });

    try
    {
        await context.CallActivityAsync("CreditAccount",
            new
            {
                Account = transferDetails.DestinationAccount,
                Amount = transferDetails.Amount
            });
    }
    catch (TaskFailedException)
    {
        // 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
            });
    }
}

Замечание

  • Сообщение об исключении обычно определяет, какие функции активности или под-оркестрации вызвали сбой. Чтобы получить более подробные сведения об ошибках, проверьте FailureDetails свойство.
  • По умолчанию FailureDetails включает тип ошибки, сообщение об ошибке, трассировку стека и все вложенные внутренние исключения (каждый из которых представлен как рекурсивный объект).FailureDetails См. в разделе Include Custom Exception Properties for FailureDetails (.NET Isolated), чтобы в выходные данные сбоя включить дополнительные свойства исключений.

Это важно

Примечание о миграции (из процесса в изолированный): В внутрипроцессной модели FunctionFailedException.InnerException содержит исходный объект исключения, выброшенный действием, который можно привести и проверить напрямую. В изолированной рабочей модели TaskFailedExceptionне содержит исходного исключения в виде InnerException. Вместо этого сведения об ошибке доступны только через FailureDetails свойство, которое предоставляет свойства на основе строк (ErrorType, , ErrorMessage). StackTrace Вы не можете напрямую преобразовывать или получать доступ к исходному объекту исключения. Используется FailureDetails.IsCausedBy<T>() для проверки исходного типа исключения.


Модель внутрипроцессного процесса

В среде выполнения Durable Functions C# необработанные исключения выбрасываются в виде FunctionFailedException.

Сообщение об исключении обычно включает функцию активности или подоркестрацию, которая завершилась сбоем. Дополнительные сведения см. 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
            });
    }
}

Замечание

В предыдущих примерах C# используется Durable Functions 2.x. Для Durable Functions 1.x используйте DurableOrchestrationContext вместо IDurableOrchestrationContext. Различия между версиями см. в статье версии Durable Functions.

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

В пакетах SDK для устойчивых задач необработанные исключения, создаваемые в действиях или вложенных оркестрациях, маршалируются обратно в оркестратор с помощью TaskFailedException типа. Свойство исключения FailureDetails содержит подробные сведения о сбое.

using Microsoft.DurableTask;

[DurableTask(nameof(TransferFundsOrchestration))]
public class TransferFundsOrchestration : TaskOrchestrator<TransferOperation, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, TransferOperation transfer)
    {
        await context.CallActivityAsync(
            nameof(DebitAccountActivity),
            new AccountOperation { Account = transfer.SourceAccount, Amount = transfer.Amount });

        try
        {
            await context.CallActivityAsync(
                nameof(CreditAccountActivity),
                new AccountOperation { Account = transfer.DestinationAccount, Amount = transfer.Amount });
        }
        catch (TaskFailedException ex)
        {
            // Log the failure details
            var details = ex.FailureDetails;

            // Compensate by refunding the source account
            await context.CallActivityAsync(
                nameof(CreditAccountActivity),
                new AccountOperation { Account = transfer.SourceAccount, Amount = transfer.Amount });

            return $"Transfer failed: {details.ErrorMessage}. Compensation completed.";
        }

        return "Transfer completed successfully";
    }
}

Если действие CreditAccount завершается сбоем, оркестратор перехватывает исключение и компенсирует, зачисляя средства обратно в исходную учетную запись.

Ошибки с несколькими вызовами задач (разветвление/слияние)

При использовании Task.WhenAll для параллельного выполнения нескольких вызовов действий (шаблон увеличения/уменьшения) и если одна или несколько действий завершаются неудачно, await возникает только первое исключение. Чтобы получить доступ ко всем сбоям, проверьте Exception свойство в Task возвращаемом объекте Task.WhenAll.

Изолированная рабочая модель
var tasks = new[]
{
    context.CallActivityAsync("Activity1", input1),
    context.CallActivityAsync("Activity2", input2),
    context.CallActivityAsync("Activity3", input3),
};

var allTask = Task.WhenAll(tasks);
try
{
    await allTask;
}
catch (TaskFailedException)
{
    // 'await' rethrows only the first exception. To inspect all failures,
    // check allTask.Exception, which is an AggregateException.
    if (allTask.Exception != null)
    {
        foreach (var inner in allTask.Exception.InnerExceptions)
        {
            if (inner is TaskFailedException taskFailed)
            {
                // Use taskFailed.FailureDetails to inspect error details
                var errorType = taskFailed.FailureDetails.ErrorType;
                var errorMessage = taskFailed.FailureDetails.ErrorMessage;
            }
        }
    }
}

Модель внутрипроцессного процесса
var tasks = new[]
{
    context.CallActivityAsync("Activity1", input1),
    context.CallActivityAsync("Activity2", input2),
    context.CallActivityAsync("Activity3", input3),
};

var allTask = Task.WhenAll(tasks);
try
{
    await allTask;
}
catch (FunctionFailedException)
{
    // 'await' rethrows only the first exception. To inspect all failures,
    // check allTask.Exception, which is an AggregateException.
    if (allTask.Exception != null)
    {
        foreach (var inner in allTask.Exception.InnerExceptions)
        {
            if (inner is FunctionFailedException funcFailed)
            {
                // Use funcFailed.InnerException to access the original exception
            }
        }
    }
}

Ошибки в функциях сущностей

Обработка исключений в функциях сущностей зависит от модели размещения среды Durable Functions.

Изолированная рабочая модель

В изолированном Durable Functions C# среда выполнения упаковывает исключения функций сущности в EntityOperationFailedException. Чтобы получить исходные данные об исключении, проверьте свойство FailureDetails.

[Function(nameof(MyOrchestrator))]
public static async Task<List<string>> MyOrchestrator(
   [Microsoft.Azure.Functions.Worker.OrchestrationTrigger] TaskOrchestrationContext context)
{
    var entityId = new Microsoft.DurableTask.Entities.EntityInstanceId(nameof(Counter), "myCounter");
    try
    {
        await context.Entities.CallEntityAsync(entityId, "Add", 1);
    }
    catch (EntityOperationFailedException ex)
    {
        // Add your error handling
    }

    return new List<string>();
}

Модель внутрипроцессного процесса

В Durable Functions с встроенным процессом C# функции сущностей возвращают исходные типы исключений оркестратору.

[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 is 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;
    }
}

Автоматическое повторение попыток при сбое

При вызове функций действий или функций подоркестрации укажите политику автоматического повтора. В следующем примере вызывается функция до трех раз и ожидается пять секунд между повторными попытками:

Изолированная рабочая модель
[FunctionName("TimerOrchestratorWithRetry")]
public static async Task Run([OrchestrationTrigger] TaskOrchestrationContext context)
{
    var options = TaskOptions.FromRetryPolicy(new RetryPolicy(
        maxNumberOfAttempts: 3,
        firstRetryInterval: TimeSpan.FromSeconds(5)));

    await context.CallActivityAsync("FlakyFunction", options: options);

    // ...
}

Модель внутрипроцессного процесса
[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);

    // ...
}

Замечание

Предыдущие примеры C# предназначены для Durable Functions 2.x. Для Durable Functions 1.x необходимо использовать DurableOrchestrationContext вместо IDurableOrchestrationContext. Дополнительные сведения о различиях между версиями см. в статье о версиях устойчивых функций .

Вызов функции активности в предыдущем примере использует параметр для настройки политики автоматического повтора. Настройте политику с помощью следующих параметров:

  • Max number of attempts (Максимальное число попыток): максимальное число попыток. Если задано значение 1, повторные попытки не возникают.
  • First retry interval (Интервал до первого повтора): время ожидания перед первой повторной попыткой.
  • Backoff coefficient (Коэффициент отсрочки): коэффициент, позволяющий определить степень увеличения отсрочки. По умолчанию равен 1.
  • Максимальный интервал повтора: максимальное время ожидания между попытками повторных попыток.
  • Максимальное время тайм-аута повторной попытки: максимальное время для попыток повторного соединения. По умолчанию повторные попытки продолжаются бесконечно.

Пакеты SDK для устойчивых задач включают альтернативные методы планирования, которые повторяют неудачные действия на основе предоставленной политики. Эти методы полезны для действий, которые считывают данные из веб-служб или выполняют идемпотентные записи в базу данных.

using Microsoft.DurableTask;

[DurableTask(nameof(OrchestratorWithRetry))]
public class OrchestratorWithRetry : TaskOrchestrator<string, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, string input)
    {
        // Configure retry policy
        var retryPolicy = new RetryPolicy(
            maxNumberOfAttempts: 3,
            firstRetryInterval: TimeSpan.FromSeconds(5),
            backoffCoefficient: 2.0,
            maxRetryInterval: TimeSpan.FromMinutes(1),
            retryTimeout: TimeSpan.FromMinutes(5));

        var options = TaskOptions.FromRetryPolicy(retryPolicy);

        // Call activity with automatic retry
        string result = await context.CallActivityAsync<string>(
            nameof(UnreliableActivity), input, options);

        return result;
    }
}

Параметры политики повторных попыток:

  • Максимальное количество попыток: максимальное число попыток повторных попыток. Если задано значение 1, повторные попытки не возникают.
  • First retry interval (Интервал до первого повтора): время ожидания перед первой повторной попыткой.
  • Backoff coefficient (Коэффициент отсрочки): коэффициент, позволяющий определить степень увеличения отсрочки. По умолчанию равен 1.
  • Максимальный интервал повтора: максимальное время ожидания между попытками повторных попыток.
  • Retry timeout (Время ожидания повтора): максимальное время, отведенное на выполнение повторных попыток.

Пользовательские обработчики повторных попыток

В .NET и Java реализуйте обработчики повторных попыток в коде, если декларативные политики повторных попыток недостаточно выражены. На других языках реализуйте логику повторных попыток с помощью циклов, обработки исключений и таймеров для задержки между повторными попытками.

Изолированная рабочая модель
TaskOptions retryOptions = TaskOptions.FromRetryHandler(retryContext =>
{
    // Don't retry anything that derives from ApplicationException
    if (retryContext.LastFailure.IsCausedBy<ApplicationException>())
    {
        return false;
    }

    // Quit after N attempts
    return retryContext.LastAttemptNumber < 3;
});

try
{
    await ctx.CallActivityAsync("FlakeyActivity", options: retryOptions);
}
catch (TaskFailedException)
{
    // Case when the retry handler returns false...
}

Модель внутрипроцессного процесса
RetryOptions retryOptions = new RetryOptions(
    firstRetryInterval: TimeSpan.FromSeconds(5),
    maxNumberOfAttempts: int.MaxValue)
{
    Handle = exception =>
    {
        // Return true to handle and retry, or false to throw.
        if (exception is TaskFailedException failure)
        {
            // Exceptions from task activities are always this type. Inspect the
            // inner exception for more details.
        }

        return false;
    }
};

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

Пользовательские обработчики повторных попыток

В .NET и Java реализуйте обработчики повторных попыток в коде для управления логикой повторных попыток. Этот подход полезен, если декларативные политики повторных попыток недостаточно выражены.

using Microsoft.DurableTask;

[DurableTask(nameof(OrchestratorWithCustomRetry))]
public class OrchestratorWithCustomRetry : TaskOrchestrator<string, string>
{
    public override async Task<string> RunAsync(
        TaskOrchestrationContext context, string input)
    {
        // Custom retry handler with conditional logic
        TaskOptions retryOptions = TaskOptions.FromRetryHandler(retryContext =>
        {
            // Don't retry if it's a validation error
            if (retryContext.LastFailure.IsCausedBy<ArgumentException>())
            {
                return false;
            }

            // Retry up to 5 times for transient errors
            return retryContext.LastAttemptNumber < 5;
        });

        try
        {
            return await context.CallActivityAsync<string>(
                nameof(UnreliableActivity), input, retryOptions);
        }
        catch (TaskFailedException)
        {
            // All retries exhausted
            return "Operation failed after all retries";
        }
    }
}

Таймауты функций

Если вызов функции занимает слишком много времени, прекратите его выполнение в функции оркестратора. Создайте устойчивый таймер с селектором any задач, как показано в следующем примере:

Изолированная рабочая модель
[Function("TimerOrchestrator")]
public static async Task<bool> Run([OrchestrationTrigger] TaskOrchestrationContext 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;
        }
    }
}

Модель внутрипроцессного процесса
[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;
        }
    }
}

Замечание

Предыдущие примеры C# предназначены для Durable Functions 2.x. Для Durable Functions 1.x необходимо использовать DurableOrchestrationContext вместо IDurableOrchestrationContext. Дополнительные сведения о различиях между версиями см. в статье о версиях устойчивых функций .

Замечание

Механизм не завершает выполнение функции действия, которая уже находится в процессе исполнения. Она позволяет функции оркестратора игнорировать результат и двигаться дальше. Дополнительные сведения см. в секции Таймеры.

Тайм-ауты действий

Если выполнение операции занимает слишком много времени, вы можете перестать его ждать. Создайте прочный таймер и сравните его с задачей активности.

using Microsoft.DurableTask;
using System;
using System.Threading;
using System.Threading.Tasks;

[DurableTask(nameof(OrchestratorWithTimeout))]
public class OrchestratorWithTimeout : TaskOrchestrator<string, bool>
{
    public override async Task<bool> RunAsync(
        TaskOrchestrationContext context, string input)
    {
        TimeSpan timeout = TimeSpan.FromSeconds(30);
        DateTime deadline = context.CurrentUtcDateTime.Add(timeout);

        using var cts = new CancellationTokenSource();
        Task activityTask = context.CallActivityAsync(nameof(SlowActivity), input);
        Task timeoutTask = context.CreateTimer(deadline, cts.Token);

        Task winner = await Task.WhenAny(activityTask, timeoutTask);
        if (winner == activityTask)
        {
            // Activity completed in time - cancel the timer
            cts.Cancel();
            return true;
        }
        else
        {
            // Timeout occurred
            return false;
        }
    }
}

Замечание

Этот механизм не завершает выполнение действия, которое уже выполняется. Он позволяет оркестратору игнорировать результат и двигаться дальше. Дополнительные сведения см. в документации по таймерам.

Необработанные исключения

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

Включите настраиваемые свойства исключений для FailureDetails (.NET изолированный)

В рабочих процессах устойчивых задач, использующих изолированную модель .NET, сбои задач сериализуются в объект FailureDetails. По умолчанию объект включает следующие поля:

  • ErrorType— имя типа исключения
  • Message— сообщение об исключении
  • StackTrace— трассировка сериализованного стека
  • InnerFailure— вложенный FailureDetails объект для внутренних исключений

Начиная с Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.9.0, можно расширить это поведение, реализуя IExceptionPropertiesProvider (определенный в пакете Microsoft.DurableTask.Worker, начиная с v1.16.1). Этот поставщик определяет, какие типы исключений и свойства следует включить в FailureDetails.Properties словарь.

Замечание

  • Эта функция доступна в только .NET Isolated. Поддержка Java пока недоступна.
  • Убедитесь, что вы используете Microsoft.Azure.Functions.Worker.Extensions.DurableTask версии 1.9.0 или более поздней версии.
  • Убедитесь, что вы используете Microsoft.DurableTask.Worker версии 1.16.1 или более поздней версии.

Реализация поставщика свойств исключения

Реализуйте настраиваемый IExceptionPropertiesProvider объект для извлечения и возврата выбранных свойств для исключений, которые вам нужны. Возвращаемый словарь сериализуется в поле PropertiesFailureDetails при возникновении соответствующего типа исключения.

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
        };
    }
}

Регистрация поставщика.

В Program.cs зарегистрируйте собственный IExceptionPropertiesProvider в изолированном хосте рабочей роли .NET:

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();

После регистрации поставщика любое исключение, соответствующее обрабатываемому типу, автоматически включает в себя FailureDetailsнастроенные свойства.

Пример выходных данных FailureDetails

При возникновении исключения, соответствующего конфигурации поставщика, оркестрация получает сериализованный объект FailureDetails в следующем виде.

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

Необработанные исключения

Если оркестратор завершается сбоем из-за необработанного исключения, среда выполнения регистрирует сведения об исключении, и экземпляр завершает выполнение со статусом Failed. TaskFailedException имеет свойство FailureDetails, включающее тип ошибки, сообщение и трассировку стека.

Дальнейшие действия