Мы в «ЛАНИТ — Би Пи Эм» занимаемся построением BPM-решений и автоматизацией бизнес-процессов. Обычно после выпуска первой версии развитие процессов не останавливается и заказчик хочет их улучшать: заменять ручные расчеты на автоматические, убирать ненужные шаги и обращения к устаревшим системам и т. п.
Автоматизированные процессы часто являются отражением реального мира и могут выполняться долго: дни, месяцы или годы. Что делать при выпуске новой версии с теми процессами, что были запущены на старой версии, но не успели завершиться? Их нужно довести до конца по старой версии или мигрировать на середину новой версии и довести уже по ней?
В этой статье я расскажу о проблемах (очевидных и не очень) обновления запущенных ранее процессов и про то, почему обычные подходы к миграции данных не работают в процессных системах на базе BPMN, и приведу несколько инструментов и идей, проверенных нами в реальных проектах на Camunda Platform и Pega Platform. Статья направлена на мидлов и старших разработчиков.
![](https://habrastorage.org/getpro/habr/upload_files/7b9/c42/56d/7b9c4256da13ecfeb360b7aaee4d19c2.png)
В чём корень проблем?
Возьмём простейший BPMN-процесс с одним ручным шагом, к которому во второй версии добавили возможность альтернативного действия:
![Старая версия | Новая версия Старая версия | Новая версия](https://habrastorage.org/getpro/habr/upload_files/a6b/d82/d92/a6bd82d92e75cf099700966b63a315de.png)
При изменении процесса, особенно если мы ничего не удаляем и не изменяем, а только добавляем элементы, интуитивно кажется, что «закон обратной силы не имеет», и старые процессы могут идти по старой версии. Предположим, что BPM-продукт, на котором построено наше решение, так и работает — запоминает версию запущенного процесса. Предположим также, что ручные шаги выполняются в веб-приложении — и у него вышла новая версия, где на форме задачи появилась кнопка «Отклонить». Что же произойдёт, когда пользователь откроет задачу из старого процесса и нажмёт на эту новую кнопку? Наше решение попробует выполнить альтернативное действие на старом процессе, где его нет, что приведёт к ошибке.
Можно было бы потребовать от BPM-продукта, чтобы он переводил все процессы на новую версию автоматически. Рассмотрим этот вариант на примере процесса, в котором в новой версии часть ручных действий была заменена автоматическим выполнением:
![Старая версия | Новая версия Старая версия | Новая версия](https://habrastorage.org/getpro/habr/upload_files/e4f/b8b/60a/e4fb8b60a066fb52952ac24742e36117.png)
Что произойдет в старых процессах, которые ожидали обработки на момент выпуска новой версии? После автоматического перевода на новую версию наше решение будет считать, что шаг с автоматической обработкой уже пройден, и шаг с ручной обработкой выполнит только свою половину работы, как положено по новой версии процесса. В результате мы получим «тихую» ошибку в виде не полностью выполненного процесса, поскольку никто не запустил автоматическую обработку «задним числом».
Стоит ещё учесть, что изменения в процессе могут быть и более драматическими:
![Возможные варианты развития процесса Возможные варианты развития процесса](https://habrastorage.org/getpro/habr/upload_files/889/76a/138/88976a1380a12e336bf1cece8dd01c84.png)
В общем случае можно считать, что новая и старая версии — это два разных процесса. Чтобы перейти из одного процесса в другой, нужно описать логику перехода — это описание называется миграцией.
Существует ещё вариант с одновременной поддержкой нескольких версий процесса, но он требует поддержания адаптеров для каждой версии в разных компонентах решения (веб-приложение, бизнес-логика). По нашему опыту, это дорогостоящий и хрупкий вариант, поэтому мы не будем его рассматривать в этой статье.
Итак, мы познакомились с причинами проблем и наметили ключевую цель — после выпуска новой версии решения все процессы должны мигрировать на самую последнюю версию. Вопрос — как этого добиться?
Нужно просто описать миграции в SQL!
Те, кто знаком с подходом Evolutionary Database Design, скажут: «Состояние процесса — это же обычные данные в реляционной БД. Нужно описать миграцию как SQL-скрипт и опубликовать его в репозитории для инструмента типа Flyway / Liquibase».
Для тех, кто не знаком с этим подходом, расскажу в двух словах:
Изменения в структуре БД описываются как инкрементные SQL-скрипты от предыдущего состояния структуры к желаемому.
Помимо DDL скрипты могут содержать DML и даже запуск процедур для выполнения миграции данных.
Скриптам присваивается порядковый номер (например, числовой префикс в названии файла скрипта), и они сохраняются в репозиторий вместе с исходным кодом.
Скрипты упаковываются в приложение и выполняются по порядку при старте приложения специальным служебным инструментом, который анализирует, какие скрипты уже были применены к этой БД.
Состояние процесса действительно представляет собой несколько строк в таблицах БД, а развитие BPMN-схемы процесса похоже на развитие структуры БД. Нюанс с состоянием процесса в том, что это не непосредственная часть вашего решения, а часть используемого BPM-продукта, его инкапсулированный компонент, вторгаться в который не совсем корректно с точки зрения проектирования. Перечислю конкретные практические минусы:
Состояние процесса состоит из точек, где остановился процесс, переменных, подпроцессов и др. — это достаточно сложный набор данных, поэтому скрипты будет тяжело писать и поддерживать.
В некоторых продуктах может применяться объектная сериализация переменных или всего состояние процесса, формат будет не обязательно общеупотребимым текстовым типа JSON, в худшем случае это может быть проприетарный бинарный формат.
При обновлении версии самого BPM-продукта в вашем решении формат хранения состояния процессов в нём может поменяться.
Получается, что сам подход выглядит применимым, но вот инструмент и способ описания миграций нам не подходят.
Тогда как описывать миграции?
Если работа с состоянием процесса напрямую на уровне БД исключается, нужно обратиться к тем средствам, которые предоставляет и рекомендует BPM-продукт.
Хорошие BPM-продукты предоставляют API, которое при описании миграций позволяет:
Изменять текущие точки остановки процесса: создавать новые, превращать их в другие точки остановки, удалять.
Запускать и останавливать подпроцессы.
Запускать шаги, которые необходимо выполнить «задним числом», например, вычисления и вызовы других систем.
Изменять переменные процесса.
Язык и детали этого API сильно зависят от BPM-продукта, иногда несколько вышеописанных задач решается одной операцией в API. Некоторые задачи миграции решаются API для выполнения прикладных действий над процессом, например: выполнение задачи, получение сообщения.
Можно выделить несколько паттернов для описания миграций, которые применимы в разных BPM-продуктах.
Паттерн Legacy Branch
Этот паттерн применим, когда новая версия не должна иметь обратной силы и запущенные процессы должны выполниться по старой версии.
![версия 1 | версия 2 (синий шаг для завершения старых процессов) версия 1 | версия 2 (синий шаг для завершения старых процессов)](https://habrastorage.org/getpro/habr/upload_files/4a4/588/ed7/4a4588ed7714f454b92d3ca3da16b88a.png)
Механизм работы:
Старый шаг / ветка остаются на новой схеме процесса наряду с актуальными шагами.
В старый шаг нет путей для входа, т.е. в него не могут прийти новые процессы.
Если нужно, отдельные шаги этой старой ветки могут быть скорректированы / удалены по сравнению с изначальным старым вариантом, чтобы корректно интегрироваться в новую версию.
Плюсы:
Крайне прост в использовании.
Реализуем практически в любом BPM-продукте.
Минусы:
Схема процесса замусоривается старыми версиями, которые можно удалить только, когда на всех стендах завершатся старые процессы.
В других компонентах решения (веб-приложение, API) необходимо также оставить поддержку старых версий.
Паттерн Migration Island
Этот паттерн применим, когда происходят изменения внутри процесса / подпроцесса: удаляется шаг, изменяется тип шага, шаг заменяется на подпроцесс. Отдельно стоит отметить применимость для ситуации, когда появляется обязательный шаг, который нужно выполнить «задним числом».
![Версия 1 | Версия 2 (синие шаги выполняются во время миграции) Версия 1 | Версия 2 (синие шаги выполняются во время миграции)](https://habrastorage.org/getpro/habr/upload_files/88b/3cb/3f0/88b3cb3f028f3c684dab08533be0e871.png)
Механизм работы:
Шаг, на котором остановился процесс, остается на схеме процесса, но без путей для входа, т.е. в него не могут прийти новые процессы.
После него, если необходимо, добавляется ветка с дополнительными шагами для миграции.
Заканчивается старый шаг / ветка переходом на тот шаг нового процесса, который является новой точки остановки.
В описании миграции старый шаг процесса программно завершается, что запускает выполнение дополнительных шагов и переход в нужную точку остановки.
Похож на Legacy Branch, но ключевое отличие в том, что старая ветка выполняется во время миграции, а не в прикладном сценарии.
Плюсы:
Можно реализовать почти любые преобразования в процессе и дополнительные шаги.
Логика миграции проста и наглядна.
Реализуем практически в любом BPM-продукте.
В других компонентах решения (веб-приложение, API) не нужно оставлять поддержку старых версий.
Минусы:
Схема процесса замусоривается деталями миграции, особенно, если в одной версии несколько таких «островов».
В аудите процесса будет запись о фиктивном выполнении старого шага.
Паттерн Migration Ticket
Если в новой версии процесса много шагов с одинаковым способом миграции или происходят структурные изменения в дереве подпроцессов, например, целый подпроцесс теряет актуальность, то можно использовать этот паттерн.
![Старая версия | Новая версия (синие шаги выполняются во время миграции) Старая версия | Новая версия (синие шаги выполняются во время миграции)](https://habrastorage.org/getpro/habr/upload_files/a5c/92d/98f/a5c92d98f099f639748455ca30486566.png)
Механизм работы:
Изменяемый шаг, набор шагов или подпроцесс остаётся на схеме процесса, но без путей для входа, т.е. в него не могут прийти новые процессы.
На нужном уровне иерархии подпроцессов добавляется обработка события — обычно это Signal / Message Boundary Event.
После него, если необходимо, добавляется ветка с дополнительными шагами для миграции.
Заканчивается старый шаг / ветка возвращением на нужный шаг нового процесса.
В описании миграции программно отправляется событие во все/определённые процессы, что отменяет выполнение любых изменяемых шагов и запускает выполнение ветки миграции.
Похож на Migration Island и на обработку ошибок типа try/catch.
Плюсы:
Позволяет запускать и останавливать подпроцессы.
Минусы:
Требует какого-то механизма событий в BPM-продукте.
Схема процесса замусоривается шагами миграции.
Что можно забыть при описании миграции?
Некоторые ошибки при описании миграций не приведут к немедленному сбою во время миграции, а оставят процессы в неконсистентном состоянии, что приведет к ошибкам при дальнейшем продвижении по процессу — это самый неприятный вид ошибок. Несколько примеров:
Не учесть асинхронные ответы, которые придут из других систем после исчезновения шага.
Не обновить переменные процесса или другие данные, сопровождающие процесс.
Не выполнить обязательные шаги «задним числом», например, не заполнить данные расчета, которые должны быть получены на добавленном в начало процесса шаге
Не запустить подпроцесс, который теперь обязательно должен сопровождать основной процесс.
Миграцию описали, можем выпускать?
Прежде чем выпускать новую версию процесса, нужно решить ещё один вопрос, который проще всего объяснить на примере.
Допустим, вы разрабатываете версию 3 процесса, при этом на промышленном стенде установлена версия 1, на тестовом — версия 2. Версия 2 вполне может никогда не добраться до прома, но она уже установлена на другой стенд, который нельзя откатить к версии 1. Каким образом следует описывать миграции — мигрировать инкрементами с версии 1 на 2, а с 2 на 3, или сразу полностью с версии 1 на 3?
Полные миграции требуют меньше времени на выполнение, но сложно предугадать, с какой на какую версию может потребоваться мигрировать. Инкрементные миграции повторяют проверенный подход эволюционного развития схемы БД, но требуют последовательного выполнения нескольких миграций для каждого процесса. Выбор зависит от специфики решений, для нас хорошо подошёл инкрементный подход.
Кто будет запускать миграции?
Для запуска миграций можно использовать следующие способы:
Ops Team вручную определяет процессы, необходимые миграции и применяет их.
Ops Team вручную запускает только фазу миграции, а определение процессов, список миграций и их применение выполняется автоматически.
Все шаги, включая запуск фазы миграции, выполняются автоматически.
По нашему опыту, чем меньше будет ручных шагов, тем лучше, но более автоматизированные способы требуют более продвинутого инструмента. Далее мы обсудим подход в реализации полностью автоматизированного способа.
Как лучше выполнять миграции?
Детали запуска и выполнения миграций зависят от BPM-продукта: какие средства администрирования он использует, доступен ли в нём планировщик, поддерживает ли он явно версионность процессов и т. д. Постараемся выделить общеупотребимые идеи.
Самый простой вариант автоматической реализации:
Запустить фазу миграции при старте приложения (listener или аналоги).
Синхронно сформировать список процессов и требуемых миграций.
Синхронно применить все миграции.
Продолжить старт приложения.
Этот способ очень популярен в тех решениях, что мы видели, и мы рекомендуем использовать его, пока позволяют масштабы.
С ростом приложения у этого варианта начинаются проблемы с масштабированием:
Определение списка процессов и миграций занимает значительное время.
Применение миграций занимает значительное время.
Применение миграций требуется балансировать по кластеру.
Логично переводить синхронные шаги в асинхронное выполнение через планировщик. При этом необходимо учитывать нюансы:
Миграции должны применяться после того, как приложение и BPMN-продукт запустились.
Запуск фазы миграции, формирование списка процессов и требуемых миграций необходимо выполнить ровно один раз, нужен кластерный планировщик.
Задачи применения миграций можно распределять между узлами кластера через кластерный планировщик.
Необходимо балансировать по узлам кластера нагрузку выполнения миграций и прикладную нагрузку, возможно, следует выделить несколько узлов исключительно под миграции.
Необходимо предусмотреть стратегию обработки ошибок на каждом из шагов.
Самая неприятная сложность асинхронного выполнения миграций — это потенциальная недоступность процессов. В промежутке между запуском приложения и завершением миграций для отдельно взятого процесса (пока процесс ожидает миграции в планировщике) попытка работать с ним в приложении может привести к ошибкам и неконсистентным данным. Может потребоваться реализовать специальную обработку входящих запросов к таким процессам: отказ с ошибкой, постановка в очередь и т. п.
Как итог мы рекомендуем использовать следующую стратегию: выполнять миграции синхронно, пока их скорость позволяет, и не открывать доступ к процессам до их завершения. Только после исчерпания возможностей синхронного выполнения переходить к более сложному асинхронному выполнению, при этом придётся решить проблему недоступности процессов.
Заключение
Запущенные процессы обновляются во время своей жизни, как и другие структуры данных в наших решениях. Чаще всего BPM-продукт не может самостоятельно определить, как выполнить эти обновления, и команде необходимо описать правила для миграции.
Миграция запущенных ранее BPM-процессов — нетривиальная задача, которая при этом не очень хорошо покрывается в популярных BPM-продуктах, даже коммерческих. Команде стоит посвятить время получению экспертизы в этом вопросе, изучить ресурсы о выбранном BPM-продукте. Постепенно в команде можно накапливать удачные подходы и готовые компоненты, чтобы снизить сложность и стоимость решения задачи миграции.
Надеюсь, статья поможет вам понять основные проблемы и пути решения задач, связанных с миграцией процессов. Буду благодарен за вопросы и дополнения в комментариях.