7/10/2011 1:04:20 PM

Шаблон “Saga” используется для моделирования long-running (Как это будет по-русски? Долгосрочные? Долгоиграющие?) бизнес-процессов. Фактически, мы можем сказать, что Saga представляет собой Workflow для какого-то определённого сценария.

Long-running не следует понимать в терминах секундной стрелки и задаваться вопросом: 200 милисекунд – это long-running или нет? В системах с архитектурой, построенной на событиях и сообщениях подобные вопросы вряд ли имеют смысл.

Для примера рассмотрим ситуацию, когда для исполнения какой-то операции требуется совершить вызов каких-то двух веб-сервисов.
Само по себе время здесь даже не очень-то и важно, важно здесь то, что мы больше не можем гарантировать transaction consistency нашей операции.
Что произойдёт в том случае, если в промежутке между этими двумя вызовами произойдёт исключение? Что произойдёт, если вызов второго веб-сервиса завершится неудачей? Что произойдёт в случае, если оба веб-сервиса сработали удачно, но мы не смогли подтвердить транзакцию в нашей собственной базе данных по каким-то причинам? (в этом случае нам даже двух веб-сервисов не нужно, достаточно одного).
Мы можем отменить нашу транзакцию (а в случае исключения это произойдёт автоматически), но мы не можем отменить наш вызов к первому веб-сервису, так, как он не является частью этой транзакции. По тем же причинам мы не можем просто повторить команду, надеясь, что уж в этот-то раз всё точно сработает успешно, так как это приведёт к повтору вызова веб-сервисов.

Вот это и есть long-running процесс. Это и есть те самые бизнес-правила (точнее, их реализация, модель), о которых я писал в постинге про CQRS.

Long-running процессы подразумевают состояние (state)

Идея, которую реализует шаблон “Saga” проста: после каждого успешно выполненного шага мы имеем некоторое состояние, с которого можно будет продолжить исполнение процесса. Шагом является выполнение какого-то дейтсвия, реакция на какое-то событие и т.д.
То есть, если мы не смогли подтвердить транзакцию в базу данных, или если вызов второго веб-сервиса завершился неудачей, у нас имеется состояние, валидное на момент до его вызова.
Наш бизнес-процесс остановлен – это да, но он и не потерян. Мы можем предпринять какие-то действия и продолжить процесс. Как результат – процесс просто выполнялся дольше.

Кроме того, имея такое состояние, мы можем легко моделировать процессы, “управляемые” событиями!

Старый пример: Если произошли оба OrderAccepted и OrderBilled, то можно приступать к отправке товара клиенту. События OrderAccepted и OrderBilled в системе могут произойти в любом порядке, с любым промежутком времени между ними.
Например, OrderAccepted начинает бизнес-процесс (Saga) для этого заказа и мы имеем некое состояние, в котором отмечаем, что это событие произошло. Когда происходит OrderBilled, Saga отмечает и это в своём состоянии.
Когда получены оба события, можно приступать к отправке заказа.

В реальности такая Saga может быть чуть более сложной:

  • Создать Saga при получении любого из событий OrderAccepted и OrderBilled.
  • При получении второго события приступить к отправке товара.
  • При получении OrderCancelled немедленно прекратить свою работу.
  • Если получено одно сообщение, но не получено второе в течение заданного времени, дать знать о том, что с заказом что-то не так в соответствующий департамент.
  • <какие-то ещё бизнес-требования>

Бизнес-процесс моделируется как Saga

Очень удобно моделировать бизнесс-процесс через шаблон Saga как минимум потому, что процесс в этом случае получается автономным. Saga реагирует на события и выполняет какие-то действия, этот процесс самодостаточен, не связан (ничем, кроме событий) с другими процессами системы.
Saga инкапсулирует своё состояние внутри себя, самостоятельно хранит в себе нужную в рамках процесса информацию о каких-то фактах (заказ оплачен, заказ подтверждён) и эта информация больше никого не касается.
Saga описывается в одном месте и легко поддерживается и обновляется (в противовес, возможно, стандартному решению: добавим к Order колонки Billed и Shipped, для вычисления промежутков времени напишем класс, который будет вызываться периодически каждые 10 секунд, проверять базу данных, как-то дёргать нужный метод и т.д).

Важный аспект: при создании Saga всегда нужно задаваться вопросом времени. Что должно случиться, если время ожидания вышло? Как долго мы можем позволить себе ждать?
Не потому, что тут есть какие-то технические проблемы в том, что Saga будет “жить” долго, а потому, что мы моделируем бизнес-процесс. А в бизнесе ничто не может длиться вечно и задержки часто означают проблемы, новые бизнес-правила и т.д.

Реализация шаблона Saga
Здесь код будет часто более описателен, чем слова, поэтому я буду приводить пример на псевдокоде.
Это не код какого-либо фреймворка, а просто описание для понятности.

Saga можно описать как:

public sealed class MySaga : Saga<MySagaState>,
        IStartedBy<Message1>,
        IMessageHandler<Message2>
{
        public override void Configure()
        {
             ConfigureForMessage<Message1>(m => m.OrderId, s => s.OrderId);
             ConfigureForMessage<Message2>(m => m.ClientOrderId, s => s.OrderId);
        }

        public void Handle(Message1 message)
        {
             this.State.SomeData = message.SomeData;
             StartTimer(Timespan.FromMinutes(25));
        }

        public void Handle(Message2 message) { StopSaga(); }

        public void TimeOut() { StopSaga(); }
}

В этом примере у нас есть Saga, которая начинается с сообщения Message1 и реагирует на сообщение Message2. Понятно, что Saga может наследовать несколько IStartedBy<T>, что будет означать, что Saga начинается с любого из этих сообщений и будет потом реагировать на оставшиеся.
Сообщение Message2 не будет начинать новую Saga, однако, если Saga уже существует, сообщение Message2 будет ею получено. Естественно, Saga может наследовать IMessageHandler<T> несколько раз.

Saga в данном случае имеет состояние типа MySagaState.

Так, как мы можем (и будем) иметь в системе не один экземпряр Saga, а множество, необходимо определить, как сообщения будут коррелировать с экземплярами Saga (а точнее, с состоянием), для чего в приведённом выше примере используется метод Configure().
С помощью него мы указываем как найти подходящее состояние для полученного сообщения: Для Message1 корреляция делается между свойства OrderId в состоянии и в самом сообщении, для Message2 должны совпасть значения свойства ClientOrderId в сообщении и свойства OrderId в состоянии.

В случае, когда получено сообщение Message1 и нет экземпляра MySagaState с соответствующей корреляцией, такой экземпляр будет создан и Saga начнёт свою работу.
В случае, когда получено сообщение Message2 и нет экземпляра MySagaState с соответствующей корреляцией, сообщение будет проигнорировано и Saga не будет начата.

Таким образом мы можем иметь только один экземпляр MySaga для каждого значения OrderId.

Методы Handle(…) для каждого сообщения определяют, что нужно сделать, когда то или иное сообщение получено.

В приведённом случае при получении Message1 мы изменяем состояние и устанавливаем таймаут в 20 минут.
При получении сообщения Message2 мы просто останавливаем Saga, то же самое делаем по таймауту. Понятно, что можно делать что-то поумнее :)

Особенности реализации Saga

Saga является неким “оркестрирующим” элементом, описательным, так сказать (В MassTransit и RhinoBus даже интерфейс называется не IMessageHandler<T>, a Orchestrate<T>). Поэтому крайне рекомендуется не производить никакой “полезной работы” в Saga. Никаких запросов к веб-сервисам, никаких запросов к базам данных сервисов системы (разумеется).
Всё, что предполагается делать в Saga – это работа над состоянием на основании событий и сообщений. Полностью messaging-ориентированная архитектура.
Всё, что может делать Saga – это дождаться какого-то сообщения и послать какое-то другое сообщение.
Если Saga что-то “хочет” для своей работы, то этого можно добиться послав команду и дождавшись ответа.
Представьте себе директора в своём кабинете с листочком бумаги и карандашом. Ему поступает инфорамация о том, что происходит в различных подразделениях компании, он что-то чёркает на листочке и периодически отдаёт указания. Когда ему нужна какая-то информация, он просит ему её доставить.
Это и есть Saga :) В промежутках между этими сообщениями менеджер-Saga занимается полезной работой – спит.

Такое требование обосновано с точки зрения декомпозиции: каждый занимается чем-то одним, Single Responsibility Principle. Saga – оркестрирует бизнес-процесс, руководит.
С точки зрения производительности это тоже обосновано: при получении сообщения Saga находит/создаёт коррелирующее состояние, вызывает соответствующий метод Handle(…) для соответствующего сообщения, после чего сохраняет состояние и завершает свою работу.
Чем быстрее Saga обработает сообщение, тем больше сообщений может быть обработано в единицу времени.

Поэтому Saga – это не очередной класс на 574 строки, который делает всё, а маленький message-ориентированный кусочек бизнес-логики :)

Аналогия
Можно провести какую-то аналогию с Windows Workflow Foundation, но в случае последнего это гораздо менее гибко с точки зрения поддержки, требует больших усилий по написанию (Saga всё же простой класс), менее гибко с точки зрения сохранения состояния (WF сохраняет состояние instance’а когда ей кажется, что instance ничего не делает) и горадо менее удобно в messaging-ориентированной среде и имеет большие проблемы с версионностью.

Версионность и Saga
Здесь всё достаточно тривиально. 
Так или иначе мы имеем два варианта изменений: “Новый вариант Saga должен заработать немедленно, мы пофиксили баг” и “Старые Saga должны закончится как есть, а вот заново должны начинаться новые варианты”.

В случае первого всё понятно: мы должны гарантировать обратную совместимость состояния и проблем больше нет.

В случае второго может быть использована такая стратегия:

  • Создаём специальный IMessageHandler, который вызовется перед Saga и проверит, существует ли уже экземпляр состояния для этой Saga.
  • Если экземпляр существует – ничего не делаем, текущий экземпляр Saga выполнит свою работу.
  • Если такой экземпляр не существует, то прекращаем обработку сообщения, а само сообщение посылаем в очередь “новой” версии Saga. То есть, фактически мы роутим сообщение в новую очередь V2.
  • Когда “старых” экземпляров не осталось, мы можем просто перевести “новую” версию Saga на ту очередь, которую слушает “старая”, а “старую” просто убрать из системы вместе с тем IMessageHandler, который осуществлял роутинг.
    Система приходит к первоначальному виду.

sagas

Промежуточный результат

Sagas как бизнес-процессы, обрабатывающие бизнес-правила и есть та часть CQRS, которая отвечает за всю внутреннюю “магию”. Состояния Sagas и есть то, что хранится в базе Command Database, в которой до появления Persisted ViewModel хранились сами данные.
Эта база размещается настолько быстро к серверам, занимающимся непосредственной бизнес-логикой, насколько можно. Эта база достаточно мала, так как хранит информацию только о бизнес-процессах, которые находятся в процессе своей работы на том или ином этапе. После того, как процесс завершён, эта информация больше не нужна и может быть удалена из БД.

А, да. Напоминание. Всё это время мы находились в пределах одного BC ;)

Вот, собственно, и всё, что я хотел написать про CQRS как таковой.

Comments (4) -

7/10/2011 1:26:28 PM

build_your_web

long-running - продолжительных.

Большое спасибо за серию статей о CQRS.

build_your_web Russia

7/15/2011 5:08:57 PM

Пашка

> Если получено одно сообщение, но не получено второе в течение заданного времени, дать знать о том, что с заказом что-то не так в соответствующий департамент.

А как вот это событие на "прошел час после получения первого сообщения" обработать? В смысле кто вот этот delay activity из windows workflow foundation должен ловить?

Пашка Russia

7/16/2011 6:53:57 AM

Alexey Raga

При чём тут windows workflow foundation?
Никаких windows workflow foundation нам тут не надо.
Saga же у нас описывает бизнес-процесс.
Saga, скажем так, "запрашивает таймаут", когда этот таймаут возникает (физически - Saga "разбужена" по таймауту, мы попадаем в метод TimeOut()) - Saga делает то, что ты в этом методе описал. То есть, отправляет куда-то какие-то сообщения и т.д.

Таймаут тут - не нечто исключительное, а просто предусмотренная часть бизнес-процесса.
Например, как я уже писал, многие интернет-магазины вообще не процессят заказы в течение первого часа-двух, так как велик шанс того, что пользователь захочет отказаться.
Вот тебк и бизнес-процесс обработки ордера, который начинается с того, что запрашивается таймаут длиной в час-два (плюс ожидание какого-нибудь OrderCancelled для того, чтобы "умереть"), по таймауту начинается нормальная обработка.

Saga в этом плане - штука самодостаточная, windows workflow foundation не нужен Smile

Alexey Raga Australia

7/16/2011 7:25:55 AM

Пашка

Да не, я просто аналог спрашивал. Уже нашел, что это шина имплементит. DelaySend для Rhino Service Bus, например.

Пашка Russia

Comments are closed

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter

Widget Twitter not found.

Root element is missing.X


Recent posts

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012

Sign in