Создание User-Friendly движка бизнес-процессов на основе Windows Workflow Foundation

в 7:55, , рубрики: .net, BPM, ECM, ECM/СЭД, workflow, Блог компании НПО «Компьютер»

Постановка задачи

Создание User-Friendly движка бизнес-процессов на основе Windows Workflow Foundation - 1

Одной из неотъемлемых частей любой ECM-системы является управление бизнес-процессами, или workflow.

Бизнес-процессы в каждой отдельной организации имеют множество нюансов. Они постоянно изменяются вследствие изменений внутри организации, изменений законодательства и т.д. Поэтому дешевле и логичнее к разработке бизнес-процессов привлекать либо аналитиков, либо программистов, специализирующихся на бизнес-логике. А значит, создание и изменение бизнес-процессов должно быть максимально простым и удобным.

Так же при изменении процесса уже запущенные процессы должны корректно работать. Нельзя останавливать долгое и сложное согласование договора только потому, что теперь согласованный документ должен распечатать не инициатор согласования, а секретарь.

Это диктует некоторые требования, которые предъявлялись к движку бизнес-процессов:

  • Процессы должны разрабатываться на основе высокоуровневых блоков. Примером такого блока может быть создание задания на согласование документа, старт подзадачи, выполнение произвольного куска кода и т.д.
  • При изменении схемы процесса нужно обеспечить возможность конвертации уже запущенных процессов на новую версию схемы.

При разработке новой версии движка бизнес-процессов мы решили попробовать Windows Workflow Foundation (далее WF).

Разработка на основе высокоуровневых блоков

Упрощение разработки бизнес-процессов

Каждый высокоуровневый блок маршрута может состоять из большого количества Activity (Например, для блока задания нужно 68 активностей). Это связано с тем, что каждый блок имеет несколько событий, в обработчики которых можно писать код. Так же для каждой части блока (события, внутренняя логика блока) должна работать обработка ошибок. Обработка эта делает следующее: если было брошено исключение, то оно анализируется, и в некоторых случаях нужно не прерывать процесс, а попытаться еще раз через некоторое время. Причем время ожидания до следующей попытки постепенно возрастает от 5 минут до 1 часа. Это нужно для ситуаций, когда не удалось совершить операцию из-за проблем со связью, таймаута SQL сервера и т.д.

Можно было бы сделать блоки составными активностями, но WF не позволяет делать активности с несколькими исходящими стрелками. Например, блок маршрута «Задание на согласование документа» должен выглядеть следующим образом:

Создание User-Friendly движка бизнес-процессов на основе Windows Workflow Foundation - 2

А WF позволяет сделать только так:

Создание User-Friendly движка бизнес-процессов на основе Windows Workflow Foundation - 3

Причем еще придется делать переменную и передавать через нее результат выполнения задания.
Вторая проблема – блоки, выполняемые параллельно. Единственный способ сделать это в WF – использовать блок Parallel. Но тогда вместо интуитивного:

Создание User-Friendly движка бизнес-процессов на основе Windows Workflow Foundation - 4

Мы получаем:
Создание User-Friendly движка бизнес-процессов на основе Windows Workflow Foundation - 5

Все это привело нас к тому, что нам не достаточно активностей 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 происходит в несколько этапов:

  1. Для старой версии Activity вызывается InstanceConverter.PrepareForUpdate. Этот вызов кэширует текущее описание схемы в нее саму же.
  2. Activity модифицируется.
  3. Для модифицированной Activity вызывается DynamicUpdateServices.CreateUpdateMap. Этот вызов создает UpdateMap – карту изменений, на основе которой конвертируются запущенные экземпляры схемы.
  4. При загрузке сохраненного экземпляра в WorkflowApplication указывается карта изменений.

Основная проблема здесь – невозможность создать UpdateMap на основе двух Activity. Т.е. если на одном сервере развернута версия 1, на другом – версия 2, на третьем – версия 3, то обновиться на версию 5 будет проблематично. Еще сложнее будет обновить первый сервер с версии 1 на версию 4.

Как мы решаем проблему с конвертацией

Схемы на сервере хранятся в виде XML, в котором лежат наши блоки, а не активности. Таким образом, конвертировать нужно с одной версии нашего представления маршрута на другую. Это происходит так:

  1. Для старой версии строится Activity.
  2. Для построенной Activity вызывается InstanceConverter.PrepareForUpdate.
  3. Строится дифф между старой версией маршрута и новой. Он состоит из добавленных, удаленных, измененных блоков и связей. Для построения корректного диффа у каждого блока и каждой связи есть свой уникальный ИД.
  4. По этому диффу изменяется подготовленная Activity.
  5. Строится карта изменений.
  6. Каждый инстанс маршрута загружается с этой картой изменений и сразу же выгружается. Это делается сразу для всех экземпляров, чтобы карта изменений использовалась ровно один раз.

Изменение Activity в пункте 4 происходит так: сгенерированная для блока активность упаковывается в FlowStep (если из блока выходит несколько стрелок с условиями, то после FlowStep генерируется FlowDecision). При изменении/добавлении/удалении связей изменяются значения свойств FlowStep.Next.

Каждый блок хранится в переменной в схеме в сериализованном виде. При изменении свойств блока меняется дефолтное значение этой переменной.

При добавлении блока генерируется соответствующий набор активностей и вставляется в нужное место схемы. Удаление блока – это просто очистка FlowStep.Next, который в него ведет.

Конвертация при изменении генерируемых активностей

Кроме изменения бизнес-процесса, конвертация может потребоваться и при изменении генерируемых для блока активностей. Например, если нужно добавить новый функционал в блок, или просто исправить баг. Мы сделали это так:
Каждая схема маршрута хранит версию алгоритма генерации Activity.
При изменении логики генерации активности для блока версия увеличивается, а конвертер учится конвертировать активности этого блока со старого варианта на новый.
При конвертации маршрута конвертер конвертирует активности блоков, для которых изменилась логика генерации (определяется по версии схемы).
Единственная особенность – конвертация так же должна проходить в виде изменения существующих активностей, а не генерации с нуля, иначе UpdateMap не подхватится.

Заключение

После прочтения статьи может создаться впечатление, что мы зря использовали Workflow Foundation – это не так. Благодаря использованию WF мы получили из коробки хостинг, хранение экземпляров процессов, всю логику выполнения процессов, в том числе и параллельного.

В статье описано лишь решение проблемы низкоуровневости WF. За кадром остались вопросы хостинга процессов, проблемы конвертации некоторых наборов Activity и многое другое.

Автор: lsreg

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js