#asynchronous #architecture #domain-driven-design #cqrs #distributed-system
Вопрос:
Давайте предположим, что у нас есть два долгоживущих менеджера процессов. Например, обе саги оперируют более чем 10 миллионами предметов. Первая сага добавляет что-то к каждому предмету. Вторая сага удаляет его из каждого предмета. Учитывая, что обоим менеджерам процессов требуется несколько минут, чтобы завершить свою работу, если я запущу их одновременно, у меня возникнут проблемы.
Часть этих предметов будет иметь ценность, в то время как остальные-нет. Результат на самом деле близок к случайному и зависит от порядка команд, которые влияют на конкретный элемент. Я задавался вопросом, решит ли проблему повторная отправка команды «Удалить» в случае сбоя. Я имею в виду, что если вы попытаетесь удалить несуществующее значение, вам следует подождать, пока первая сага добавит значение. Но пока менеджеры процессов работают, кто-то другой может отправить команду «Удалить» или «Добавить». В таком случае мой подход потерпел бы неудачу.
Как я могу решить такую проблему? 🙂
Ответ №1:
Похоже, что вы хотели бы, чтобы вторая сага не запускалась, если запущена первая сага (и, предположительно, не запускалась до тех пор, пока не произойдет какой-либо процесс, который зависит от того, что добавила первая сага). Таким образом, очевидным решением было бы наличие компонента (может быть микросервисом, также может быть записью в строго согласованном хранилище данных, таком как zookeeper/etcd/consul), который дает разрешение на запуск саг. Пример протокола может выглядеть следующим образом:
- Saga отправляет сообщение компоненту, идентифицирующему сагу и сообщающему о намерении начать
- Компонент проверяет, что никакие саги не могут быть запущены, что помешало бы запуску этой саги
- Компонент отвечает разрешением на запуск
- Последующие попытки саги приводят к отклонению до тех пор, пока запущенная сага не сообщит компоненту, что можно запустить другую сагу
Предполагая, что этот компонент надежно надежен, режим сбоя, о котором следует беспокоиться, заключается в том, что разрешение предоставлено, но этот компонент никогда не обрабатывает сообщение о завершении саги (причины этого могут включать сообщение о разрешении, которое не доставляется/обрабатывается, или сбой саги). Никакое количество подтверждений или дополнительных сообщений не может решить эту проблему (в основном это проблема двух генералов).
Смягчение заключается в том, чтобы этот компонент (или что-то, наблюдающее за этим компонентом) предупреждал, если кажется, что прошло слишком много времени без завершения саги. Независимо от того, кто/кто несет ответственность за обеспечение живучести, затем изучит, работает ли сага по-прежнему, и если она не запущена, сообщите компоненту, что можно запустить другую сагу. Обратите внимание, что это не является надежным: вполне возможно, что решающий, о котором идет речь, примет неправильное решение.
Комментарии:
1. спасибо вам за ваш замечательный ответ. Я принял такое решение во внимание, но, честно говоря, оно мне не очень нравится из-за его блокировочной манеры. Было бы идеально придумать решение, позволяющее любому человеку выполнять собственные действия без необходимости ждать. Возможно, даже математически невозможно достичь такого, или что смешанный результат одновременного выполнения обеих саг совершенно справедлив. Я не уверен, что с точки зрения домена такое поведение имеет большой смысл. Я просто инстинктивно чувствую боль перед лицом необходимости блокировки приложения в асинхронном мире 🙂
2. Ограничение двух саг, работающих на одних и тех же объектах, управляемых одной и той же службой, затрудняет это. Возможно, было бы возможно, чтобы все, для чего нужны данные, добавленные первой сагой, отрабатывали свое собственное представление объектов, при этом данные всегда были там (это был бы более эффективный подход CQRS).
3. @ayeo Расширением этого подхода было бы совместное использование процессов в одном и том же согласованном состоянии, поэтому, когда пользователь инициирует массовое обновление, он добавит операции в этот общий документ (фактически пользовательскую очередь команд с логикой приоритета) и начнет выполнение команд, когда второй пользователь инициирует свое массовое обновление, он добавит команды в ту же очередь (документ), которая определит порядок выполнения команд относительно ожидаемого результата. Это все равно заблокирует, но только отдельные команды, а не весь процесс.
Ответ №2:
Я чувствую, что мне нужно больше контекста. Хотя вы не говорите об этом явно, проблема в том, что вторая сага пытается удалить значения, которые не были добавлены первой?
Если бы это было правдой, простым решением было бы просто использовать третье состояние.
Что я имею в виду под этим, так это просто более четко определить и объявить состояние элемента. В настоящее время у вас , похоже , есть два состояния with value
и without value
, но ничего, что указывало бы, готов ли элемент к обработке второй сагой, потому что первая сага уже выполнила свою работу над рассматриваемым элементом.
Так что все, что должно произойти, — это то, что вторая сага продолжает искать предметы, где:
(with_value == true amp; ready_for_saga2 == true)
Ready_for_saga2
или «Обработка Saga 1 завершена», что кажется более подходящим в вашем контексте.
Комментарии:
1. удаление/добавление данных-это всего лишь пример. Вообще говоря, я имею в виду ситуацию, когда две саги влияют на один и тот же предмет, и конечное состояние этого предмета зависит от порядка команд. В более сложной системе было бы даже трудно обнаружить проблему — взаимное влияние на агрегат может быть незначительным. Я просто хотел бы в конечном итоге управлять согласованным состоянием набора элементов.
2. Если вам нужно обрабатывать в определенном порядке, то у вас не может быть 2 независимых процесса, потому что это создает «условия гонки» (об этом много написано, должно быть легко найти).
Ответ №3:
Я бы сказал, что решение будет варьироваться в зависимости от того, какую реальную проблему мы пытаемся решить.
Скажем, это инвентарь, и добавить-это товары, добавленные в инвентарь, а удалить-товары, запрошенные для доставки. Тогда порядок команд не имеет большого значения, потому что вы можете просто обработать запрос на доставку, когда в инвентарь добавляются новые товары.
Это приведет к агрегированному корневому каталогу с двумя коллекциями: Items и PendingOrders.
Один менеджер процессов добавляет новые запасы к Товарам — если какие-либо заказы находятся на рассмотрении, он выполнит эти заказы в одной транзакции и удалит как товар, так и заказ из коллекций.
Если другой менеджер процессов добавляет заказ (пытается удалить товар), он либо сделает это сразу, если остались какие — либо товары, либо добавит заказ в отложенные заказы, которые будут обработаны при поступлении новых товаров (и, возможно, уведомит кого-нибудь о задержке, пока мы этим занимаемся).
Таким образом, мы в конечном итоге получаем одно и то же состояние независимо от порядка команд, но реальная проблема в реальном мире оказывает большое влияние на выбранную модель.
Если у нас есть другие проблемы в реальном мире, мы можем создать модель и для них.
Предположим, у вас есть два пользователя, каждый из которых запускает процесс массового обновления названий элементов инвентаря. В этом случае вы — и пользователи — должны решить, как лучше разрешить этот конфликт — что приведет к наилучшему результату в реальном мире.
Если вам нужна согласованность во всех элементах — все элементы или никакие элементы не должны обновляться одним массовым обновлением — я бы включил эти знания в новую модель. Давайте назовем это UpdateTitlesProcesses
. У нас есть только один экземпляр этой модели в системе. Состояние распределяется между процессами. Эта модель фактически представляет собой очередь команд, и когда пользователь инициирует массовую операцию, он добавляет все команды в очередь и начинает обрабатывать каждый элемент по одной за раз.
Когда второй пользователь инициирует очередное обновление заголовка, бизнес-логика в наших моделях отклонит это, так как уже запущено еще одно обновление. Или, если эксперты говорят, что последняя запись должна выиграть, мы исключаем оставшиеся команды из первого процесса и добавляем новые (и точно так же мы должны решить, что произойдет, если пользователь выпустит одно обновление заголовка, а не массовое — его следует отклонить, приоритизировать или отложить?).
Так что вкратце я бы сказал:
- Дайте понять, какую проблему реального мира мы решаем — и, следовательно, какой результат разрешения конфликта является лучшим (вероятно, компромисс, часто также требующий взаимодействия с пользователем или уведомления).
- Смоделируйте это явно (где процессы, действия и обработка конфликтов также являются частью модели).