Первой мыслью по решению задачи, было: создать под каждый аккаунт свою отдельную очередь в RabbitMQ и таким образом сохранить порядок обработки сообщений. Идея быстро была отброшена потому, что:
- Будет большое количество очередей, которые будут расти с ростом пользователей
- Будет необходимо держать большое количество обработчиков для этих очередей (хотя это и можно оптимизировать, но все равно будет лишняя и не нужная работа)
Вторая мысль: добавить шардирование. Иметь пул очередей и распределять сообщения между ними вычисляя шард от пользователя, таким образом гарантируя последовательность обработки.
Она мне понравилась больше, так как мы получим предсказуемое кол-во очередей и воркеров.
Итого мы получаем следующий флоу:
- Api endpoint, который получает событие, привязанное к пользователю
- Это событие отправляется в очередь с именем, вычесленным от id пользователя
- Обработчик этой очереди получает и обрабатывает событие
Основной минус - вычисление шарда. Могут возникнуть ситуации перегрузки одной или нескольких очередей в моменте, когда остальные пустуют. Это проблему можно решить оптимизацией вычисления шарда.
Третья мысль: использовать базу данных для хранения и расределения событий вместо шардирования. Да это будет медленнее, но зато надежно. Плюс, если порядок выполнения будет определятся на стороне клиента и api будет принимать порядковый номер события для выполнения, то это решение позволит гарантировать последовательность выполнения событий, даже если первое событие какого-то пользователя система получит самым последним. Такая ситуация выходит за рамки условия задачи, поэтому я все же остановился на втором варианте.
Выполнение:
К сожалению, получилось так, что у меня не было возможности развернуть рабочую среду для выполненения задания. На руках был только свежий ненастроенный шторм и гит. Поэтому решение - скорее псевдокод, призванный показать как бы я пробовал подойти к решению задачи и как выделял бы области взаимодействия. Понимаю, что это не совсем то, что нужно, но надеюсь этого хватит. Раньше, чем через несколько дней у меня доступа к рабочей среде не будет. Фактически задание я сделал в дороге, на чужом ноуте.
HomeController::processEvent() - получение событий и отправка их в очередь. EventCommand::handle(%QueueName%) - воркер для конкретной очереди (предполагаю, что количество воркеров будет управлятся, например супервизором или чем-то подобным).
EventApp - условная бизнес логика ProcessEventRequest - условная валидация