Как стать автором
Обновить

Комментарии 4

Таким образом, в конце выполнения саги, независимо от успешности, мы получаем консистентные данные во всех доменах.

Вы получаете ложную надежду на консистентность и никаких гарантий. Сомневаюсь, что все ваши источники данных поддерживают двухфазный комит или хотя бы транзакции. А в этой ситуации ни о каком ACID речи быть не может. Попытка спрятать реальность за фасадом какого-то API эту самую реальность не изменит. До вас с сагами были (и это только небольшая часть) JTA/XA, EJB, DCOM, COM+ и Corba. Все они пытались сделать распределенные транзакции, ничего из этого не взлетело. Причины не являются тайной, но обсуждать их тут было бы слишком длинно.

Чем вот это
public class ExampleSaga : SagaBase<ExampleContext>
{
  public ExampleSaga()
  {
    Step("Step 1")
      .WithAction(c => ...)
      .WithCompensation(c => ...);
	
    AsyncStep("Step 2")
      .WithAction(async c => ...);
  }
}

var saga = new ExampleSaga();
var context = new ExampleContext();
await saga.Execute(context);


лучше чем вот это?
var context = new ExampleContext();
try 
{
    await Step1Action(context);
}
catch (Exception)
{
    await Rollback(context);
}
await Step2Action(context);


При этом в одном приложении может быть зарегистрировано несколько провайдеров одновременно, что позволяет, например, в рамках одного сервиса без каких-либо затрат на доработку инфраструктуры провести пошаговую миграцию с одной СУБД на другую.


Получается, данные из разных БД смешиваются в одном контексте? Как с транзакциями быть в этой ситуации?

Совершенно непонятно зачем поверх абстракции над БД надстраивать еще одну с аналогичной функциональностью? Если хочется прозрачной миграции, то это можно сделать без привлечения такой большой и сложной прослойки.
interface IDbContext 
{ 
    IQueryable<MsSqlEntity> Entity1 {get;}
    IQueryable<OracleEntity> Entity2 {get; }  

    Task SaveAsync();
}

class DbContext : IDbContext 
{
    public async Task SaveAsync()
    {
        await oracleContext.SaveAsync();
        await msSqlContext.SaveAsync();
    }
}


Спасибо за внимание, ждём ваших комментариев и pull request-ов!

Друзья, кажется вы местами изобретаете велосипед с квадратными колесами, упражняясь в архитектурной астронавтике. О сагах и БД я выше написал, вещи вроде CallContext нужны почти везде, но конкретно ваша реализация подходит только вам. Аналогично ваша обертка над log4net нужна только вам. Я бы не ждал, что вокруг этого кода возникнет какое-то сообщество, все это слишком специфично и иногда спорно.

Мне кажется, шансы на распространение вашего кода значительно вырастут если он не будет оборачивать обертки (типа обертки над log4net) и будет более независимым от всего остального. Например я бы хотел взять CallContext и доработать его под себя. Но он зависит от ViennaNET.WebApi.Net, ViennaNET.WebApi.Abstractions и SimpleInjector. А мне все это совсем не нужно и выбирая между перспективной написать свой call context по мотивам вашего или вычищать ваш от зависимостей я выбираю первое, оно понятнее и предсказуемее.
Спасибо за комментарий.
Да, сагу можно развернуть в набор конструкций с try-catch, но это будет выглядеть довольно громоздко, когда необходимо сделать значительное количество шагов. Вот, например, развернутая сага из трех шагов:
Пример кода
var context = new ExampleContext();

try
{
  await Step1Action(context);
}
catch (Exception ex)
{
}

try
{
  await Step2Action(context);
}
catch (Exception ex)
{
  await RollbackStep1(context);
}

try
{
  await Step3Action(context);
}
catch (Exception ex)
{
  await RollbackStep2(context);
  await RollbackStep1(context);
}


По своей сути сага в нашей библиотеке содержит лишь довольно простой алгоритм для вызова действий в определенной последовательности, не более. Поэтому на обеспечение консистентности работают уже конкретные реализации этих действий.
Например, у нас есть проект, где в рамках бизнес-процесса задействуются 6 сервисов, в каждом сервисе используется модель хранения данных в виде событий, а значит, что для изменнения данных в рамках одного сервиса делается только insert в БД. В этом случае сага состоит из 6-ти основных шагов, где выполняются локальные доменные действия и делаются insert-ы событий во временные таблицы. Далее в этой же саге выполняется еще 6 шагов, где созданные записи переносятся в основные таблицы. В случае отката, например, если на шаге 4 не пройдет валидация данных, события из временных таблиц сервисов 1-3 будут удалены. По сути это некая реализация двухфазного коммита. В качестве транспорта используется RabbitMQ с настройками, гарантирующими доставку сообщения. Так что, консистентность здесь вполне достижима.

По поводу оберток и зависимостей. Да, мы это понимаем, поэтому у нас есть задачи в бэклоге, которые нацелены как раз на переход к нативным механизмам и абстракциям платформы (например, тот же ILogger), а также на более глубокую декомпозицию функционала.
Я бы записал это так
await using var tx = new ExampleTransaction();
try 
{
    await tx.Step1();
    await tx.Step2();
    await tx.Step3();    
}
catch (Exception e)
{
    await tx.Rollback(); //let tx figure out what to rollback.
}

Это почти то же самое, но лучше композируется с другими примитивами потока управления типа for/if/switch и т.п. Когда-то давно я делал много DSL подобных вашей саге, неизменно разочаровывался в результате из-за всяких нюансов связанных с потоком управления. Это справедливо и для большинства библиотек валидации.

Что касается распределенных транзакций, то по вашему ответу уже очевидно, что саги к нему имеют довольно отдаленное отношение, ключевым является event sourcing.

По поводу оберток и зависимостей. Да, мы это понимаем, поэтому у нас есть задачи в бэклоге, которые нацелены как раз на переход к нативным механизмам и абстракциям платформы (например, тот же ILogger), а также на более глубокую декомпозицию функционала.

Искренне желаю успехов :)
Это очень похоже на MassTransit и Automatonymous. Там так же саги работают на основе оркестрации и конечного автомата. Полезно, круто, но можно было бы взять готовый инструмент.

P.S. Однажды разглядывал результаты http запросов у яндекс.музыки, и однажды столкнулся с тем как случайно поля масстранзита попадали в бд, потом это пофиксил. 6-8 месяцев назад увидел такую картину в респонсах у яндекс.музыки. Эт к тому что они используют MassTransit + Automatonymous
Зарегистрируйтесь на Хабре, чтобы оставить комментарий