Откат изменений базы данных в долговременной функции
Допустим, у меня есть следующая оркестровка:
[FunctionName("Orchestration")]
public static async Task Orchestration_Start([OrchestrationTrigger] DurableOrchestrationContext ctx)
{
await ctx.CallActivityAsync("Foo");
await ctx.CallActivityAsync("Bar");
await Task.WhenAll(ctx.CallActivityAsync("Baz"), ctx.CallActivityAsync("Baz"));
}
Во всех моих действиях используется база данных SQL Azure, и в случае сбоя любого из вызовов я хочу отменить все изменения, сделанные предыдущими действиями, например, если второй вызов Baz
выдает исключение, я хочу отменить все, что сделано Foo
, Bar
и если первый Baz
завершена, я хочу отменить его модификации тоже.
В приложении без функций я мог бы просто обернуть все тело оркестровки в using scope = new TransactionScope()
блок.
Будет ли это работать для потенциально распределенной оркестровки, и если нет, то есть ли аналогичный механизм в структуре функций Azure? Или я должен написать реализацию отката для каждого из действий и зафиксировать изменения в базе данных после выполнения каждого из них?
1 ответ
Durable Functions реализуют механизм возможной согласованности. Это совершенно иное понятие, чем другие виды согласованности (например, сильная), поскольку оно гарантирует, что транзакция будет в конечном итоге завершена. Что это значит?
Используя TransactionScope
Вы можете убедиться, что если что-то пойдет не так в транзакции, откат будет выполнен автоматически. В Durable Function это не так - у вас нет автоматической функции, которая дает вам такую функциональность - фактически, если второе действие из вашего примера завершится неудачно, вы получите несогласованные данные, хранящиеся в базе данных.
Чтобы реализовать транзакцию в таком сценарии, вы должны попытаться / поймать возможную проблему и выполнить логику, которая позволит вам устранить ошибку:
[FunctionName("Orchestration")]
public static async Task Orchestration_Start([OrchestrationTrigger] DurableOrchestrationContext ctx)
{
try
{
await ctx.CallActivityAsync("Foo");
await ctx.CallActivityAsync("Bar");
await Task.WhenAll(ctx.CallActivityAsync("Baz"), ctx.CallActivityAsync("Baz"));
}
catch(Exception)
{
// Do something...
}
}
Существует также возможность реализовать политику повторов, чтобы избежать временных ошибок:
public static async Task Run(DurableOrchestrationContext context)
{
var retryOptions = new RetryOptions(
firstRetryInterval: TimeSpan.FromSeconds(5),
maxNumberOfAttempts: 3);
await ctx.CallActivityWithRetryAsync("FlakyFunction", retryOptions, null);
// ...
}
Однако важно понять, как среда выполнения Durable Functions действительно управляет ситуацией, когда что-то идет не так. Предположим, что следующий код не работает:
[FunctionName("Orchestration")]
public static async Task Orchestration_Start([OrchestrationTrigger] DurableOrchestrationContext ctx)
{
await ctx.CallActivityAsync("Foo");
await ctx.CallActivityAsync("Bar"); // THROWS!
await Task.WhenAll(ctx.CallActivityAsync("Baz"), ctx.CallActivityAsync("Baz"));
}
Если вы воспроизведете всю оркестровку, первое действие (с пропущенным "Foo") не будет выполнено еще раз - его состояние будет сохранено в хранилище, поэтому результат будет сразу же доступен. Среда выполнения выполняет контрольную точку после каждого действия, поэтому состояние сохраняется и оно знает, где оно завершилось ранее.
Теперь, чтобы правильно обработать ситуацию, вы должны реализовать следующий алгоритм:
- выполнить откат вручную при обнаружении исключения
- если это не удается, отправьте сообщение, например, в очередь, которая затем обрабатывается вручную кем-то, кто понимает, как работает процесс
Хотя изначально это может выглядеть как большой недостаток, на самом деле это идеальное решение - ошибки действительно случаются, поэтому всегда рекомендуется избегать переходных (с помощью повторных попыток), но если откат не удался, это ясно указывает на что-то не так в вашей системе.
Выбор за вами - будь у вас сильная согласованность и вам приходится иметь дело с проблемами с масштабируемостью, или вы используете более свободную модель, которая обеспечивает лучшую масштабируемость, но с которой труднее работать.