Как обрабатывать побочные эффекты в CRQS при воспроизведении событий?

10

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

Но что, если одно из событий должно привести к тому, что внешняя система, не находящаяся под вашим контролем, «отправит товар» клиенту, если вы просто воспроизведите события, товар будет отправлен дважды.

Как вы решаете это?

Jas
источник

Ответы:

6

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

Не забывайте, что шаг повторного развертывания означает на самом деле сброс состояния считанной модели на более ранний момент времени. Это, вероятно, ничего нельзя сделать (или нужно сделать) для состояния внешних систем.

Док Браун
источник
«Не забывайте, что шаг повторного развертывания означает на самом деле сброс состояния модели чтения на более ранний момент времени. Вероятно, вы ничего не можете (или не должны) делать для состояния внешних систем». -> но что, если я хочу, чтобы мой повтор повторял неудачные внешние системные вызовы, такие как доставка? в этом случае мое повторное развертывание повтора не только сбросило бы состояние прочитанной модели, но и вызвало внешние события, это звучит справедливо или я что-то упустил?
Jas
2
@Jas: вы не хотите злоупотреблять «воспроизведением», чтобы повторить неудачный внешний системный вызов. Вы используете «воспроизведение» для получения модели чтения вашей собственной системы в том же состоянии, в котором она была раньше. Это означает, что в случае неудачного запроса на доставку ваша система ранее была проинформирована об этом сбое и хранила эту информацию где-то в своем состоянии. Повтор гарантирует, что эта информация все еще там после "повторного развертывания и воспроизведения". Таким образом, после воспроизведения ваша система может применить стратегию «повторная доставка в случае сбоя» (которая не имеет ничего общего с CQRS, любая надежная система заказов просто должна иметь такую ​​стратегию).
Док Браун
Интересно, это то, что я собирался сделать, было просто интересно, есть ли «шаблон» на этом, чтобы я не изобретал колесо!
Jas
3

Из статьи Мартина Фаулера о событиях :

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

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

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

devnull
источник
2

Но что, если одно из событий должно привести к тому, что внешняя система, не находящаяся под вашим контролем, «отправит товар» клиенту, если вы просто воспроизведите события, товар будет отправлен дважды.

Чтобы выбрать конкретный пример, давайте рассмотрим, как может работать «хотя бы один раз» подход к побочным эффектам.

State currentState = State.InitialState
for(Event e : events) {
    currentState = currentState.apply(e)
}
for(SideEffect s : currentState.querySideEffects()) {
    performSideEffect(s)

Таким образом, модель предметной области отслеживает, что необходимо сделать; но оставляет фактическое выполнение для приложения

В контексте выполнения команды основная идея выглядит одинаково. Фактические побочные эффекты происходят вне транзакции, которая обновляет модель.

Так что юнит-тесты для вашей модели могут выглядеть примерно так

{
    // Given
    State currentState = State.InitialState

    // When
    Events events = List.of(OrderPlaced)

    // Then
    List.of(SendEmail) === currentState.applyAll(events).querySideEffects()
}

{
    // Given
    State currentState = State.InitialState

    // When
    Events events = List.of(OrderPlaced, EmailSent)

    // Then
    List.EMPTY === currentState.applyAll(events).querySideEffects()
}

Основными моментами здесь являются

  • Обновление модели без побочных эффектов; фактические побочные эффекты происходят вне транзакции, которая обновляет модель.
  • Событие, описывающее исход побочного эффекта, должно вернуться.
VoiceOfUnreason
источник