Следует ли при использовании MDA различать идемпотентные и неидемпотентные обработчики событий?

Вопрос предполагает использование Event Sourcing.

При восстановлении текущего состояния путем воспроизведения событий обработчики событий должны быть идемпотентными. Например, когда пользователь успешно обновляет свое имя пользователя, может быть сгенерировано событие UsernameUpdated, содержащее строковое свойство newUsername. При восстановлении текущего состояния соответствующий обработчик событий получает событие UsernameUpdated и задает для свойства username объекта User значение свойства newUsername объекта события UsernameUpdated. Другими словами, обработка одного и того же сообщения несколько раз всегда дает один и тот же результат.

Однако как работает такой обработчик событий при интеграции с внешними сервисами? Например, если пользователь хочет сбросить свой пароль, объект User может генерировать событие PasswordResetRequested, которое обрабатывается частью кода, выдающей третьей стороне команду на отправку SMS. Теперь, когда приложение пересобрано, мы НЕ хотим повторно отправлять эту смс. Как лучше избежать этой ситуации?


person magnus    schedule 13.12.2015    source источник


Ответы (3)


Во взаимодействии участвуют два сообщения: команды и события.

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

В вашем сценарии я мог бы сказать совокупному корню 100 раз, чтобы обновить имя пользователя:

public UserNameChanged ChangeUserName(string username, IServiceBus serviceBus)
{
    if (_username.Equals(username))
    {
        return null;
    }

    serviceBus.Send(new SendEMailCommand(*data*));

    return On(new UserNameChanged{ Username = userName});
}

public UserNameChanged On(UserNameChanged @event)
{
    _username = @event.UserName;

    return @event;
}

Приведенный выше код приведет к одному событию, поэтому его воссоздание не приведет к дублированию обработки. Даже если бы у нас было 100 событий UserNameChanged, результат все равно был бы таким же, поскольку метод On не выполняет никакой обработки. Я предполагаю, что нужно помнить, что командная сторона выполняет всю реальную работу, а событийная сторона используется только для изменения состояния объекта.

Вышеизложенное не обязательно является способом реализации обмена сообщениями, но оно демонстрирует концепцию.

person Eben Roux    schedule 14.12.2015
comment
В часто задаваемых вопросах CQRS (cqrs.nu/Faq/command-handlers) побочные эффекты (не идемпотентное поведение), например, отправка электронного письма должна быть в обработчике событий, а не в обработчике команд из-за проблем с параллелизмом. - person magnus; 14.12.2015
comment
Я согласен с этим. Вам нужно быть осторожным. В моем примере вы заметите, что я не отправляю электронное письмо. Я посылаю команду. Я ожидаю, что инфраструктура обмена сообщениями гарантирует, что сообщение будет обработано только один раз, и при этом правильно. Таким образом, в случае сбоя обработки команды SendEMailCommand обычно никуда не денется, поэтому электронная почта не будет отправлена. Если бы я отправил электронное письмо напрямую через SMTP, тогда да, возникла бы проблема. - person Eben Roux; 17.12.2015

Я думаю, что вы смешиваете здесь два разных понятия. Первый — это реконструкция объекта, где обработчиками являются все внутренние методы самого объекта. Пример кода из платформы Axon

public class MyAggregateRoot extends AbstractAnnotatedAggregateRoot {

@AggregateIdentifier
private String aggregateIdentifier;
private String someProperty;

public MyAggregateRoot(String id) {
    apply(new MyAggregateCreatedEvent(id));
}

// constructor needed for reconstruction
protected MyAggregateRoot() {
}

@EventSourcingHandler
private void handleMyAggregateCreatedEvent(MyAggregateCreatedEvent event) {
    // make sure identifier is always initialized properly
    this.aggregateIdentifier = event.getMyAggregateIdentifier();
    // do something with someProperty
}

}

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

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

См. документацию по платформам Axon, чтобы лучше понять проблему и решение, с которым они работали.

Воспроизведение событий в кластере

person Songo    schedule 13.12.2015

TLDR; сохранить идентификатор SMS в самом событии.

Основным принципом источника событий является «идемпотентность». События являются идемпотентными, а это означает, что обработка их несколько раз будет иметь тот же результат, что и при однократной обработке. Команды являются «неидемпотентными», что означает, что повторное выполнение команды может иметь разные результаты для каждого выполнения.

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

Одним из решений, которое я придумал, является включение UUID SMS, которое скоро будет создано, в событие PasswordResetRequested. Это позволяет диспетчеру процессов создавать SMS только в том случае, если он еще не существует, что обеспечивает идемпотентность.

Пример кода ниже (псевдокод C++):


// The event indicating a password reset was successfully requested.
class PasswordResetRequested : public Event {
public:
    PasswordResetRequested(const Uuid& userUuid, const Uuid& smsUuid, const std::string& passwordResetCode);

    const Uuid userUuid;
    const Uuid smsUuid;
    const std::string passwordResetCode;
};

// The user aggregate root.
class User {
public:

    PasswordResetRequested requestPasswordReset() {
        // Realistically, the password reset functionality would have it's own class 
        // with functionality like checking request timestamps, generationg of the random
        // code, etc.
        Uuid smsUuid = Uuid::random();
        passwordResetCode_ = generateRandomString();
        return PasswordResetRequested(userUuid_, smsUuid, passwordResetCode_);
    }

private:

    Uuid userUuid_;
    string passwordResetCode_;

};

// The process manager (aka, "saga") for handling password resets.
class PasswordResetProcessManager {
public:

    void on(const PasswordResetRequested& event) {
        if (!smsRepository_.hasSms(event.smsUuid)) {
            smsRepository_.queueSms(event.smsUuid, "Your password reset code is: " + event.passwordResetCode);
        }
    }

};

Есть несколько замечаний по поводу приведенного выше решения:

Во-первых, хотя существует (очень) низкая вероятность того, что SMS UUID могут конфликтовать, это действительно может произойти, что может вызвать несколько проблем.

  1. Связь с внешней службой запрещена. Например, если пользователь «bob» запрашивает сброс пароля, который генерирует SMS UUID «1234», то (возможно, 2 года спустя) пользователь «frank» запрашивает сброс пароля, который генерирует тот же SMS UUID «1234», процесс менеджер не поставит SMS в очередь, потому что думает, что оно уже существует, поэтому Фрэнк его никогда не увидит.

  2. Неверная отчетность в модели чтения. Поскольку существует повторяющийся UUID, сторона чтения может отображать SMS, отправленное «bob», когда «frank» просматривает список SMS, отправленных ему системой. Если повторяющиеся UUID были сгенерированы в быстрой последовательности, возможно, что «frank» сможет сбросить пароль «bob».

Во-вторых, перемещение генерации SMS UUID в событие означает, что вы должны сообщить агрегату User о функциях PasswordResetProcessManager (но не о самом PasswordResetManager), что увеличивает связанность. Однако связь здесь слабая, поскольку User не знает, как ставить SMS в очередь, а только то, что SMS должно ставиться в очередь. Если бы класс User сам отправлял SMS, вы могли бы столкнуться с ситуацией, когда событие SmsQueued сохраняется, а событие PasswordResetRequested нет, что означает, что пользователь получит SMS, но сгенерированный код сброса пароля не будет сохранен на пользователя, поэтому ввод кода не приведет к сбросу пароля.

В-третьих, если событие PasswordResetRequested генерируется, но система дает сбой до того, как PasswordResetProcessManager сможет создать SMS, то SMS в конечном итоге будет отправлено, но только при повторном воспроизведении события PasswordResetRequested (что может произойти через много времени в будущем). Например, «возможная» часть окончательной согласованности может быть далеко.


Приведенный выше подход работает (и я вижу, что он также должен работать в более сложных сценариях, таких как OrderProcessManager, описанный здесь: https://msdn.microsoft.com/en-us/library/jj591569.aspx). Тем не менее, я очень хочу услышать, что другие люди думают об этом подходе.

person magnus    schedule 14.12.2015