Когда Сценарий Выполняется Наполовину: Проектирование с Учётом Частичного Сбоя в .NET. Начало
Одна из повторяющихся ошибок во многих кодовых базах — дублированное списание средств. С клиента списали деньги один раз, но наша система посчитала, что платёж не удался. Платёжный провайдер снял деньги, заказ вернулся в состояние черновика, и пользователь получил сообщение «Платёж не удался, пожалуйста, повторите попытку». Одно действие пользователя привело к тому, что каждая подсистема получила разное мнение о том, что произошло на самом деле. Сценарий использования выглядит как транзакция, потому что он находится в одном вызове метода. Но как только он затрагивает более одной системы, вы сталкиваетесь с возможностью частичных сбоев.
Код, который выглядит нормально
Вот типичный сценарий «размещения заказа»:
internal sealed class PlaceOrder(
IOrderRepository orders,
IPaymentService payments,
IEmailService email,
IUnitOfWork uow)
{
public async Task<Result> ExecuteAsync(
PlaceOrderRequest or, CancellationToken ct)
{
var order = Order.Create(or.CustomerId, or.Items);
orders.Insert(order);
await payments
.ChargeAsync(order.Id, order.Total, ct);
await email
.SendOrderConfirmationAsync(order.Id, ct);
await uow.SaveChangesAsync(ct);
return Result.Success();
}
}
В этом методе есть три побочных эффекта, скрытых за, казалось бы, единой транзакционной границей, без какой-либо координации между ними.
Если SaveChangesAsync вызывает исключение после успешного выполнения ChargeAsync, вы списали деньги клиента и потеряли заказ. Если SendOrderConfirmationAsync вызывает исключение, заказ сохраняется, и платёж проходит, но email не отправляется. А если вы попытаетесь повторить попытку, вы спишете деньги дважды. Этот вариант использования «работает», пока не ломается, а когда ломается, он, как правило, каждый раз завершается с разной ошибкой.
Три категории побочных эффектов
Прежде чем написать хотя бы одну строку кода восстановления, классифицируйте каждый побочный эффект по одной из 3 категорий:
- Транзакционные — находятся внутри транзакции вашей БД. Вставки, обновления, события домена, отправляемые в процессе.
- Внешние и обратимые — вызов API, который вы можете компенсировать. Платёж → возврат. Резервирование запасов → освобождение.
- Внешние и необратимые — отправленные email, веб-хуки, SMS-сообщения. Как только они отправлены, их не вернуть.
Категория определяет стратегию. Не существует единого правила «правильной обработки ошибок», которое охватывало бы все три.
Окончание следует…
Источник: https://www.milanjovanovic.tech/blog/when-your-use-case-half-succeeds-designing-for-partial-failure-in-dotnet