Постановка задачи
Одной из неотъемлемых частей любой ECM-системы является управление бизнес-процессами, или workflow.
Бизнес-процессы в каждой отдельной организации имеют множество нюансов. Они постоянно изменяются вследствие изменений внутри организации, изменений законодательства и т.д. Поэтому дешевле и логичнее к разработке бизнес-процессов привлекать либо аналитиков, либо программистов, специализирующихся на бизнес-логике. А значит, создание и изменение бизнес-процессов должно быть максимально простым и удобным.
Так же при изменении процесса уже запущенные процессы должны корректно работать. Нельзя останавливать долгое и сложное согласование договора только потому, что теперь согласованный документ должен распечатать не инициатор согласования, а секретарь.
Это диктует некоторые требования, которые предъявлялись к движку бизнес-процессов:
- Процессы должны разрабатываться на основе высокоуровневых блоков. Примером такого блока может быть создание задания на согласование документа, старт подзадачи, выполнение произвольного куска кода и т.д.
- При изменении схемы процесса нужно обеспечить возможность конвертации уже запущенных процессов на новую версию схемы.
При разработке новой версии движка бизнес-процессов мы решили попробовать Windows Workflow Foundation (далее WF).
Разработка на основе высокоуровневых блоков
Упрощение разработки бизнес-процессов
Каждый высокоуровневый блок маршрута может состоять из большого количества Activity (Например, для блока задания нужно 68 активностей). Это связано с тем, что каждый блок имеет несколько событий, в обработчики которых можно писать код. Так же для каждой части блока (события, внутренняя логика блока) должна работать обработка ошибок. Обработка эта делает следующее: если было брошено исключение, то оно анализируется, и в некоторых случаях нужно не прерывать процесс, а попытаться еще раз через некоторое время. Причем время ожидания до следующей попытки постепенно возрастает от 5 минут до 1 часа. Это нужно для ситуаций, когда не удалось совершить операцию из-за проблем со связью, таймаута SQL сервера и т.д.
Можно было бы сделать блоки составными активностями, но WF не позволяет делать активности с несколькими исходящими стрелками. Например, блок маршрута «Задание на согласование документа» должен выглядеть следующим образом:
А WF позволяет сделать только так:
Причем еще придется делать переменную и передавать через нее результат выполнения задания.
Вторая проблема – блоки, выполняемые параллельно. Единственный способ сделать это в WF – использовать блок Parallel. Но тогда вместо интуитивного:
Мы получаем:
Все это привело нас к тому, что нам не достаточно активностей WF как таковых, нам нужна схема более высокого порядка, которая описывает маршрут «сверху». При разработке маршрута используются наши классы блоков (никак не связанные с WF), а уже потом готовая схема конвертируется в Activity. Схемы процессов хранятся в виде XML, генерация Activity происходит в момент публикации маршрута на сервер приложения. Кроме блоков схемы содержат связи между блоками (стрелки из одного блока в другой).
Преобразование блоков в Activity
Для каждого блока есть парный класс билдера, который генерирует активность. Выглядит это примерно так:
public override System.Activities.Activity BuildContent()
{
var result = new Variable<bool>(this.Block.ResultVariableName);
return new Sequence()
{
Variables =
{
result
},
Activities =
{
new Assign
{
To = new OutArgument<bool>(this.result),
Value = new InArgument<bool>(false)
},
//...
new Persist()
}
};
}
Составные активности мы не используем, чтобы не иметь проблем с конвертацией.
Единственная сложность в конвертации маршрута, описанного нашими блоками, состоит в параллельных ветках. Такие ветки маршрута обрабатываются отдельно, потом результат объединяется в Parallel.
Конвертация уже запущенного процесса на новую схему
Конвертация в WF
Конвертация процесса WF происходит в несколько этапов:
- Для старой версии Activity вызывается InstanceConverter.PrepareForUpdate. Этот вызов кэширует текущее описание схемы в нее саму же.
- Activity модифицируется.
- Для модифицированной Activity вызывается DynamicUpdateServices.CreateUpdateMap. Этот вызов создает UpdateMap – карту изменений, на основе которой конвертируются запущенные экземпляры схемы.
- При загрузке сохраненного экземпляра в WorkflowApplication указывается карта изменений.
Основная проблема здесь – невозможность создать UpdateMap на основе двух Activity. Т.е. если на одном сервере развернута версия 1, на другом – версия 2, на третьем – версия 3, то обновиться на версию 5 будет проблематично. Еще сложнее будет обновить первый сервер с версии 1 на версию 4.
Как мы решаем проблему с конвертацией
Схемы на сервере хранятся в виде XML, в котором лежат наши блоки, а не активности. Таким образом, конвертировать нужно с одной версии нашего представления маршрута на другую. Это происходит так:
- Для старой версии строится Activity.
- Для построенной Activity вызывается InstanceConverter.PrepareForUpdate.
- Строится дифф между старой версией маршрута и новой. Он состоит из добавленных, удаленных, измененных блоков и связей. Для построения корректного диффа у каждого блока и каждой связи есть свой уникальный ИД.
- По этому диффу изменяется подготовленная Activity.
- Строится карта изменений.
- Каждый инстанс маршрута загружается с этой картой изменений и сразу же выгружается. Это делается сразу для всех экземпляров, чтобы карта изменений использовалась ровно один раз.
Изменение Activity в пункте 4 происходит так: сгенерированная для блока активность упаковывается в FlowStep (если из блока выходит несколько стрелок с условиями, то после FlowStep генерируется FlowDecision). При изменении/добавлении/удалении связей изменяются значения свойств FlowStep.Next.
Каждый блок хранится в переменной в схеме в сериализованном виде. При изменении свойств блока меняется дефолтное значение этой переменной.
При добавлении блока генерируется соответствующий набор активностей и вставляется в нужное место схемы. Удаление блока – это просто очистка FlowStep.Next, который в него ведет.
Конвертация при изменении генерируемых активностей
Кроме изменения бизнес-процесса, конвертация может потребоваться и при изменении генерируемых для блока активностей. Например, если нужно добавить новый функционал в блок, или просто исправить баг. Мы сделали это так:
Каждая схема маршрута хранит версию алгоритма генерации Activity.
При изменении логики генерации активности для блока версия увеличивается, а конвертер учится конвертировать активности этого блока со старого варианта на новый.
При конвертации маршрута конвертер конвертирует активности блоков, для которых изменилась логика генерации (определяется по версии схемы).
Единственная особенность – конвертация так же должна проходить в виде изменения существующих активностей, а не генерации с нуля, иначе UpdateMap не подхватится.
Заключение
После прочтения статьи может создаться впечатление, что мы зря использовали Workflow Foundation – это не так. Благодаря использованию WF мы получили из коробки
В статье описано лишь решение проблемы низкоуровневости WF. За кадром остались вопросы
Автор: lsreg