- PVSM.RU - https://www.pvsm.ru -
Привет! Меня зовут Лев, и я инженер в новосибирской команде интеграционных сервисов ДомКлик. Мы разрабатываем (микро)сервисы, которые связывают между собой множество разрозненных систем, а также делают многие процессы быстрыми и прозрачными для конечного пользователя.
Мы используем ставший уже стандартным стек: Kotlin, Spring Boot, Hibernate, Liquibase и т. д. И нам для наших сервисов (на тот момент пока ещё одного) потребовался механизм исполнения бизнес-процесса. Требования к нему были следующие:
каждое действие как отдельный независимый модуль;
stateless-движок -> stateful-задача;
перезапуск некорректно отработавших задач заранее заданное количество раз;
возможность горизонтального масштабирования;
механизм должен быть асинхронным;
разработать его нужно максимально быстро и просто.
Мы немного подсмотрели структуру сервиса у наших коллег, что-то добавили от себя, и получился у нас простейший движок следующего вида. Четыре базовых SpringService: JobProcessor
, JobRunner
, JobService
и LaunchService
, а также базовая сущность задачи — JobEntity
. Разберём их подробнее. Самое первое и главное — сущность задачи (Job), вокруг которой построен весь механизм. Она имеет следующие поля:
id
— идентификатор задачи;
status
— статус исполнения задачи (PENDING
, READY
, INPROGRESS
, COMPLETED
, ERROR
);
runCount
— текущее количество попыток исполнения текущей задачи;
delayedTime
— время, спустя которое можно повторить исполнение задачи (возрастает для каждой следующей попытки);
archived
— признак нахождения задачи в архиве;
request
— тут хранится сериализованный (JSON) запрос на исполнение задачи.
Поскольку наш сервис должен был быть асинхронным, да ещё и разрабатывался максимально срочно, мы использовали PostgreSQL в качестве персистентной очереди запросов. Запрос порождает в базе задачу.
Исполнение вышеописанной задачи отдаётся следующим спринг-сервисам:
JobProcessor
имеет лишь один метод process(jobs: List<Long>)
, принимающий на вход список идентификаторов задач и запускающий их на исполнение методом run
сервиса jobRunner
.
JobRunner
тоже имеет лишь один метод run
, вызываемый из JobProcessor
. Он получает из jobService
список задач по идентификаторам, проверяет, не превышено ли максимальное количество попыток вызова, и запускает задачу в launchService
.
LaunchService
имеет один метод launch
. Именно в нём и выполняются все бизнес-операции. При успешном исполнении статус задачи переводится в COMPLETED
, иначе, в зависимости от значения runCount
, возвращается в READY
или завершается со статусом ERROR
.
JobService
— основной сервис для работы с сущностью задачи. Он может класть в БД новую задачу, выбирать из БД задачу по идентификатору и по статусу, менять статус.
Поскольку количество действий в бизнес-задачах начало расти, а при падении по каким-либо причинам сервис заново повторял весь процесс для задачи (особенно если на каком-то этапе было взаимодействие со stateful-сервисом), мы решили разделять бизнес-процесс на логические модули, назвав их фазами (Phases). Таким образом, сущность задачи получила новое поле phase
и метод next()
, возвращающий нам следующую фазу исполнения. Кроме того, JobService стал немного умнее и выбирает выполняемую над задачей операцию в зависимости от её текущей фазы. Теперь мы могли перезапускать процесс в случае ошибки не с самого начала, а лишь с фазы, на которой произошла ошибка. К тому же при горизонтальном масштабировании разные фазы задачи могут выполняться разными экземплярами сервиса, поэтому в случае падения одного или нескольких экземпляров сервиса задача будет подхвачена оставшимися.
Теперь процесс исполнения запроса выглядел так. Контроллер сервиса вызывается по REST API и формирует в базе данных в таблице с задачами новую запись. Исполнением задач занимается SpringService JobRunner
:
Берёт задачу в статусе READY
.
Переводит её в статус IN_PROGRESS
и записывает.
Смотрит на фазу, и в зависимости от её значения выполняет какое-то действие. Для передачи данных и сохранения результата используется поле context
, сериализованное в JSON.
Если финальная фаза, то сервис переводит задачу в статус COMPLETE
, сохраняет в базу и отправляет сообщение об успехе сервису-инициатору. Если фаза не финальная, то сервис переводит задачу в следующую фазу со статусом READY
.
В случае ошибки:
Если значение retries
достигло maxRetries
, то отвечаем сервису-инициатору ошибкой и возврашаем в базу задачу со статусом ERROR
.
Если значение retries
меньше maxRetries
, то делаем retries++ и возвращаем в базу со статусом READY
.
Наши сервисы разрастались, переходы между фазами переставали быть линейными, сценарии работы усложнялись, а код launchService
и JobEntity.next()
начал становиться нечитаемым и трудноподдерживаемым. С этим надо было что-то делать, и мы сделали! Ранее у нас был опыт работы с Activity, так что мы решили использовать этот BPMN-движок вместо последовательности фаз.
BPMN (Business Process Model and Notation) — нотация для описания бизнес-процессов, позволяющая представлять их визуально.
В интернете существует множество инструкций, как начать работать с Activity, так что здесь я это описывать не буду. Расскажу лишь про опыт использования. Главным достоинством стала возможность визуально представить процесс выполнения нашего бизнес-конвейера. Логика формирования данных в зависимости от условий теперь была более ясная, и это сократило количество ошибок при разработке. Например, нам требуется формировать разные пакеты документов для разных типов клиентов по сложному набору критериев. И когда таких критериев и типов клиентов становится слишком много, такие ветвления в коде уже перестают восприниматься визуально, а ошибки плодятся в разы быстрее. И тут нас выручает наглядная графическая схема процесса. Теперь LaunchService
лишь выбирал и запускал нужный нам сценарий, а вся логика переходов была на картинке.
Это было лишь переходным этапом, и приведя сервис к такой конструкции, мы начали понемногу выпиливать наш кастомный движок. Оставили все бизнес-процессы на Activity, чьи сценарии запускались уже из контроллера. В дальнейшем их обработку полностью отдали Activity.
Однако это решение привнесло с собой и некоторые недостатки. Главными из них стали:
Сложность отладки кода блоков. В Activity используется groovy-script, а сами схемы описываются огромными XML со встроенным groovy-script. Так мы получили трудноотлаживаемую часть проекта, разбросанную по *.bpmn xml-файлам.
Наличие в проекте дополнительного языка (Groovy) усложнило поддержку проекта.
Нет никакой валидации groovy-script в задаче Activity, а значит можно легко опечататься в названии метода или сигнатуре. При сборке не получится проверить валидность лишь модульными тестами.
Проект не выглядит живым, на заведенную нами уже более года назад багу (https://github.com/Activiti/Activiti/issues/2911) ответ не получен до сих пор.
Конкуренты (Camunda, Flowable) предоставляют более удобную и широкую функциональность и поддержку (а вот об этом в другой раз).
Автор: xpres
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/360174
Ссылки в тексте:
[1] Источник: https://habr.com/ru/post/535344/?utm_source=habrahabr&utm_medium=rss&utm_campaign=535344
Нажмите здесь для печати.