Миграция экземпляров процесса

Когда разворачивается новая версия определения процесса, экземпляры процесса, запущенные на предыдущих версиях, не затрагиваются: новое определение процесса к ним автоматически не применяется. Если экземпляры процесса должны продолжить выполнение по другому определению процесса, используйте API миграции экземпляров процесса.

Миграция состоит из двух частей:

  1. Создание плана миграции, который описывает, как переносить экземпляры процесса с одного определения процесса на другое.

  2. Применение плана миграции к набору экземпляров процесса.

План миграции состоит из инструкций миграции, которые по сути сопоставляют активити двух определений процесса. Инструкция связывает активити исходного определения процесса, с которого переносятся экземпляры, с активити целевого определения процесса, на которое они переносятся. Инструкция миграции гарантирует, что экземпляр исходной активити станет экземпляром целевой активити. План миграции полон, если инструкции есть для всех активных исходных активити.

Инструкции миграции предназначены для сопоставления семантически эквивалентных активити. Поэтому миграция как можно меньше вмешивается в состояние экземпляров активности, и переход проходит незаметно. Например, перенесенная пользовательская задача не переназначается. Для исполнителя миграция почти незаметна: задачу, начатую до миграции, можно успешно завершить после нее. Тот же принцип действует и для остальных типов элементов BPMN.

Если активити семантически не эквивалентны, рекомендуется сочетать миграцию с API модификации экземпляров процесса, например отменить экземпляр активности до миграции и запустить новый после нее.

Если не оговорено иное, далее в разделе API и эффекты миграции показаны на следующих моделях процессов:

Процесс exampleProcess:1:

BPMN-диаграмма примера

Процесс exampleProcess:2:

BPMN-диаграмма примера

Пример миграции экземпляра процесса

План миграции создается через точку входа API RuntimeService#createMigrationPlan. Метод возвращает fluent-построитель плана миграции. Для нашего примера код выглядит так:

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapActivities("assessCreditWorthiness", "assessCreditWorthiness")
  .mapActivities("validateAddress", "validatePostalAddress")
  .mapActivities("archiveApplication", "archiveApplication")
  .build();

Каждый вызов mapActivities задает инструкцию миграции: экземпляры первой активити должны стать экземплярами второй.

Предположим, что экземпляр процесса находится в следующем состоянии экземпляров активности:

ProcessInstance
├── Archive Application
└── Assess Credit Worthiness
    └── Validate Address

Чтобы перенести этот экземпляр процесса по заданному плану миграции, вызовите метод API RuntimeService#newMigration:

MigrationPlan migrationPlan = ...;
List<String> processInstanceIds = ...;

runtimeService.newMigration(migrationPlan)
  .processInstanceIds(processInstanceIds)
  .execute();

Состояние экземпляров активности после миграции:

ProcessInstance
├── Handle Application Receipt
│   └── Archive Application
└── Assess Credit Worthiness
    └── Validate Postal Address

Что произошло:

  • добавлен экземпляр встроенного подпроцесса Handle Application Receipt, соответствующий новому подпроцессу в exampleProcess:2;

  • экземпляры активности Archive Application, Assess Credit Worthiness и Validate Postal Address перенесены.

Что означает второй пункт? Для этих экземпляров активности есть инструкции миграции, поэтому они переносятся. Сущности, из которых состоит экземпляр, обновляются и ссылаются на новую активити и новое определение процесса. При этом экземпляры активности, экземпляры задач и экземпляры переменных сохраняются.

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

API

Ниже приведен структурированный обзор Java API для миграции экземпляров процесса. Эти операции доступны и через REST.

Создание плана миграции

План миграции создается методом API RuntimeService#createMigrationPlan. Он определяет, как выполняется миграция. План миграции содержит идентификаторы исходного и целевого определений процесса и список инструкций миграции. Инструкция миграции сопоставляет активити исходного определения процесса с активити целевого определения процесса.

Например, следующий код создает корректный план миграции:

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapActivities("assessCreditWorthiness", "assessCreditWorthiness")
  .mapActivities("validateAddress", "validatePostalAddress")
  .build();

Сопоставлять можно любые активити. Инструкции миграции должны связывать активити одного типа.

Поддерживаемые виды связи между активити:

  • один к одному.

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

Кроме того, план миграции проверяется перед выполнением, чтобы убедиться, что его можно применить к конкретному экземпляру процесса. Например, инструкции для некоторых типов активити поддерживаются только для экземпляров перехода (transition instance), то есть активных асинхронных продолжений, но не для экземпляров активности. Подробнее см. в разделе Проверка при выполнении.

Ограничения проверки

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

Инструкция связи один к одному

MigrationPlanBuilder#mapActivities(String sourceActivityId, String targetActivityId)

Инструкция связи один к одному означает, что экземпляр исходной активити переносится в экземпляр целевой активити. При миграции сохраняется состояние экземпляров активности, задач и переменных.

Обновление триггеров событий

При миграции событий можно решить, обновлять ли соответствующие триггеры событий. Подробнее см. особенности BPMN для событий. При создании плана миграции эту настройку можно задать для инструкций между событиями и для инструкций по пользовательским задачам со слушателями задачи типа timeout. Для этого служит метод updateEventTrigger. Например, следующий код создает инструкцию миграции для граничного события и обновляет его триггер во время миграции.

Условные события

Для условных событий вызов #updateEventTrigger обязателен.

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapActivities("userTask", "userTask")
  .mapActivities("boundary", "boundary")
    .updateEventTrigger()
  .build();

Установка переменных в экземпляры процесса

Иногда после переноса экземпляров процесса на новую версию определения процесса нужно добавить переменные. Например, если в новой модели процесса появилось входное сопоставление, которому нужна переменная, отсутствующая в переносимом экземпляре процесса. Переменные устанавливаются в область (scope) экземпляра процесса.

Вызов Java API:

Map<String, Object> variables = Variables.putValue("my-variable", "my-value");
MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapEqualActivities()
  .setVariables(variables)
  .build();
Известное ограничение

Сейчас нельзя устанавливать transient-переменные при асинхронной миграции. При синхронной миграции transient-переменные устанавливать можно.

Генерация плана миграции

Кроме ручного задания всех инструкций миграции, MigrationPlanBuilder умеет генерировать инструкции для всех равных активити исходного и целевого определений процесса. Тогда вручную остается описать только активити, которые не равны.

Две активити равны, если:

  • у них один тип;

  • у них одинаковый идентификатор;

  • они находятся в одной области, то есть их родительские области BPMN равны по этому же определению. Определения процессов всегда равны.

Рассмотрим фрагмент кода:

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapEqualActivities()
  .mapActivities("validateAddress", "validateProcessAddress")
  .build();

Он генерирует инструкции миграции для равных активити, здесь это assessCreditWorthiness, и добавляет сопоставление validateAddress с validateProcessAddress.

Обновление триггеров событий

Как и для отдельных инструкций, для сгенерированных инструкций миграции можно задать флаг обновления триггера события. Для этого служит метод updateEventTriggers. Его вызов равнозначен вызову updateEventTrigger для каждой сгенерированной инструкции миграции между событиями.

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapEqualActivities()
  .updateEventTriggers()
  .build();

Выполнение плана миграции

План миграции применяется к набору экземпляров процесса исходного определения процесса методом API RuntimeService#newMigration.

Миграцию можно выполнить синхронно (с блокировкой) или асинхронно (без блокировки) с помощью пакетной операции (batch).

Синхронная миграция подходит, если:

  • экземпляров процесса немного;

  • миграция должна быть атомарной, то есть выполняться сразу и завершаться ошибкой, если хотя бы один экземпляр процесса перенести нельзя.

Асинхронная миграция подходит, если:

  • экземпляров процесса много;

  • каждый экземпляр процесса нужно переносить независимо от остальных, то есть в отдельной транзакции;

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

Выбор экземпляров процесса для миграции

Экземпляры процесса для миграции задаются списком идентификаторов или запросом экземпляров процесса. Можно указать и то, и другое: список идентификаторов и запрос. Тогда переносится объединение полученных множеств.

Список экземпляров процесса

Экземпляры процесса, которые нужно перенести по плану миграции, можно задать списком идентификаторов:

MigrationPlan migrationPlan = ...;

List<String> processInstanceIds = ...;

runtimeService.newMigration(migrationPlan)
  .processInstanceIds(processInstanceIds)
  .execute();

Для фиксированного числа экземпляров процесса есть удобный метод с переменным числом аргументов:

MigrationPlan migrationPlan = ...;

ProcessInstance instance1 = ...;
ProcessInstance instance2 = ...;

runtimeService.newMigration(migrationPlan)
  .processInstanceIds(instance1.getId(), instance2.getId())
  .execute();
Запрос экземпляров процесса

Если экземпляры заранее неизвестны, их можно выбрать запросом экземпляров процесса:

MigrationPlan migrationPlan = ...;

ProcessInstanceQuery processInstanceQuery = runtimeService
  .createProcessInstanceQuery()
  .processDefinitionId(migrationPlan.getSourceProcessDefinitionId());

runtimeService.newMigration(migrationPlan)
  .processInstanceQuery(processInstanceQuery)
  .execute();

Пропуск слушателей и сопоставлений ввода-вывода

Во время миграции экземпляры активности могут завершаться или появляться заново. По умолчанию для них вызываются слушатели выполнения и сопоставления ввода-вывода соответствующих активити. Такое поведение нужно не всегда.

Например, если слушателю выполнения для работы нужна переменная, а в экземплярах исходного определения процесса ее нет, вызов слушателя полезно пропустить.

Для этого в API есть методы #skipCustomListeners и #skipIoMappings:

MigrationPlan migrationPlan = ...;
List<String> processInstanceIds = ...;

runtimeService.newMigration(migrationPlan)
  .processInstanceIds(processInstanceIds)
  .skipCustomListeners()
  .skipIoMappings()
  .execute();

Синхронное выполнение миграции

Для синхронной миграции используется метод execute. Он блокирует выполнение до завершения миграции.

MigrationPlan migrationPlan = ...;
List<String> processInstanceIds = ...;

runtimeService.newMigration(migrationPlan)
  .processInstanceIds(processInstanceIds)
  .execute();
В OpenBPM Engine метод execute() помечен как устаревший (@Deprecated). Используйте пакетную операцию executeAsync(), а execute() оставьте для тестов.

Миграция успешна, если удалось перенести все экземпляры процесса. Какие проверки выполняются перед применением плана миграции, описано в разделе Проверка.

Асинхронное выполнение миграции пакетной операцией

Для асинхронной миграции используется метод executeAsync. Он сразу возвращает ссылку на пакетную операцию, которая выполняет миграцию.

MigrationPlan migrationPlan = ...;
List<String> processInstanceIds = ...;

Batch batch = runtimeService.newMigration(migrationPlan)
  .processInstanceIds(processInstanceIds)
  .executeAsync();

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

Если задание миграции завершается с ошибкой, исполнитель заданий повторяет его, а когда повторные попытки исчерпаны, создается инцидент. Тогда для завершения пакетной миграции нужно вмешательство: увеличить число повторных попыток задания или удалить задание. Удаление отменяет миграцию конкретного экземпляра, но больше никак не влияет на пакетную операцию.

Пакетная миграция в гетерогенном кластере

Как описано в разделе Исполнитель заданий, процессный движок можно использовать в гетерогенном кластере, где развертывания распределены по узлам неравномерно. Исполнитель заданий, учитывающий развертывания (deployment-aware), выполняет задания только для зарегистрированных у него развертываний. В гетерогенном кластере это избавляет от проблем с доступом к ресурсам развертывания.

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

API и эффекты для элементов BPMN

Эффект миграции зависит от типов активити в модели процесса.

Задачи

Пользовательская задача

При миграции пользовательской задачи сохраняются все свойства экземпляра задачи (io.openbpm.bpm.engine.task.Task), кроме идентификатора определения процесса и ключа определения задачи. Задача не инициализируется заново: исполнитель, имя и другие атрибуты не меняются.

Слушатели задачи типа timeout

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

  • если слушатель задачи типа timeout с одинаковым id есть и в исходном, и в целевом определении процесса, его постоянный триггер события (таймер) переносится;

  • если слушателя задачи типа timeout из исходного определения процесса с таким id нет в целевом определении, его триггер события удаляется во время миграции;

  • если слушатель задачи типа timeout целевого определения не участвует ни в одной инструкции миграции как целевой, во время миграции для него создается новый триггер события.

Задача получения

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

Внешняя задача

При миграции активной внешней задачи сохраняются все свойства экземпляра внешней задачи (io.openbpm.bpm.engine.externaltask.ExternalTask), кроме идентификатора активити, ключа и идентификатора определения процесса. В частности, топик и состояние блокировки не меняются.

Активити, реализованные как внешние задачи, можно сопоставлять друг с другом, даже если у них разные типы. Например, внешнюю задачу отправки можно сопоставить с внешней сервисной задачей.

Шлюзы

Включающий и параллельный шлюзы

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

Кроме того, должны выполняться условия:

  • у целевого шлюза не меньше входящих потоков управления, чем у исходного;

  • есть корректная инструкция миграции для области, в которой находится шлюз;

  • на каждый шлюз целевого определения процесса сопоставлено не больше одного шлюза исходного определения процесса.

Событийный шлюз

Чтобы перенести экземпляр событийного шлюза, план миграции должен содержать инструкцию, сопоставляющую его с другим событийным шлюзом. Чтобы перенести триггеры событий шлюза (подписки на события, задания), можно сопоставить и события, следующие за шлюзом. Семантика инструкций между событиями описана в разделе События.

События

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

При сопоставлении событий есть два варианта настройки:

  1. Триггер события остается прежним. Даже если у целевого события другой триггер (например, изменена настройка таймера), перенесенный экземпляр события срабатывает по исходному определению. Это поведение по умолчанию при вызове migrationBuilder.mapActivities("sourceTask", "targetTask").

  2. Триггер события обновляется. Перенесенный экземпляр события срабатывает по целевому определению. Такое поведение задается вызовом migrationBuilder.mapActivities("sourceTask", "targetTask").updateEventTrigger().

События таймера

#updateEventTrigger для события таймера не учитывает время, прошедшее до миграции. Триггер события сбрасывается по настройкам целевого события.

Рассмотрим два процесса, в которых меняется настройка граничного события:

Процесс timerBoundary:1:

BPMN-диаграмма примера

Процесс timerBoundary:2:

BPMN-диаграмма примера

Инструкция migrationBuilder.mapActivities("timer", "timer").updateEventTrigger() заново инициализирует задание таймера. В результате граничное событие сработает через десять дней после миграции. Без updateEventTrigger настройка задания таймера сохраняется, и событие сработает через пять дней после запуска активити, когда бы ни выполнялась миграция.

Условные события

Для миграции условных событий вызов #updateEventTrigger обязателен. Условие заменяется условием нового условного события.

Граничное событие

Граничные события можно сопоставлять между исходным и целевым определениями процесса вместе с активити, к которым они прикреплены. Правила:

  • если граничное событие сопоставлено, его постоянный триггер (для таймеров, условных событий, сообщений и сигналов) переносится;

  • если граничное событие исходного определения процесса не сопоставлено, его триггер удаляется во время миграции;

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

Стартовое событие

Стартовые события событийных подпроцессов сопоставляются по тем же правилам, что и граничные события:

  • если стартовое событие сопоставлено, его постоянный триггер (для таймеров, условных событий, сообщений и сигналов) переносится;

  • если стартовое событие исходного определения процесса не сопоставлено, его триггер удаляется во время миграции;

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

Промежуточное перехватывающее событие

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

Событие компенсации

Миграция событий компенсации

При миграции экземпляров процесса с активными подписками на компенсацию действуют правила:

  • соответствующие перехватывающие события компенсации должны быть сопоставлены;

  • после миграции компенсацию можно вызвать из той же перенесенной области, что и до миграции, или из ближайшей перенесенной родительской области;

  • чтобы сохранить снимки переменных родительских областей, эти области тоже нужно сопоставить.

Экземпляры процесса с активными подписками на компенсацию переносятся сопоставлением соответствующих перехватывающих событий компенсации. Так API миграции узнает, какой обработчик компенсации исходной модели процесса соответствует какому обработчику целевой модели.

Исходный процесс:

BPMN-диаграмма примера

Целевой процесс:

BPMN-диаграмма примера

Пусть экземпляр процесса находится в состоянии:

ProcessInstance
└── Assess Credit Worthiness

У экземпляра процесса есть подписка на компенсацию для Archive Application. Поэтому корректный план миграции должен содержать сопоставление для граничного события компенсации. Например:

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("compensationProcess:1", "compensationProcess:2")
  .mapActivities("archiveApplication", "archiveApplication")
  .mapActivities("compensationBoundary", "compensationBoundary")
  .build();

После миграции компенсацию можно вызвать из той же области, что и до миграции (а если эта область удалена, то из ближайшей переносимой родительской области). Для иллюстрации возьмем исходный процесс:

BPMN-диаграмма примера

И целевой процесс:

BPMN-диаграмма примера

Если перенести экземпляр процесса в том же состоянии, что и в примере выше, внутреннее событие компенсации не вызовет компенсацию активити Archive Application. Ее вызовет только внешнее событие компенсации.

Активная компенсация

Миграция экземпляров процесса с активными обработчиками компенсации пока не поддерживается.

Добавление событий компенсации

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

Процесс compensation:1:

BPMN-диаграмма примера

Процесс compensation:2:

BPMN-диаграмма примера

Пусть до миграции экземпляр процесса находится в состоянии:

ProcessInstance
└── Assess Credit Worthiness

Если перенести этот экземпляр процесса (сопоставив Assess Credit Worthiness с ее аналогом), то вызванная после этого компенсация не затронет Archive Application.

Подпроцесс

Если к встроенному, событийному или транзакционному подпроцессу применяется инструкция миграции, он переносится в целевой подпроцесс целевого определения процесса. Состояние подпроцесса, например переменные, сохраняется. Если ни одна инструкция не применяется, экземпляр отменяется до выполнения миграции. Если в целевом определении процесса есть новые подпроцессы, в которые не переносится ни один существующий экземпляр, они при необходимости создаются во время миграции.

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

Действие по вызову

Действия по вызову переносятся как любые другие активити. Вызванный экземпляр процесса не меняется. Его можно перенести отдельно.

Маркеры элементов потока

Множественный экземпляр

Активные активити с множественными экземплярами можно перенести, если:

  • целевая активити тоже имеет множественные экземпляры того же вида (параллельные или последовательные);

  • целевая активити не имеет множественных экземпляров.

Миграция активити с множественными экземплярами

При переносе экземпляров активити с множественными экземплярами в другую такую же активити план миграции должен содержать две инструкции. Первая для внутренней активити, то есть активити с характеристиками цикла множественного экземпляра. Вторая для тела множественного экземпляра (multi-instance body). Тело представляет собой область BPMN, которая содержит внутреннюю активити и не отображается на диаграмме. По соглашению его идентификатор имеет вид <id внутренней активити>#multiInstanceBody. При переносе тела множественного экземпляра вместе с внутренней активити состояние множественного экземпляра сохраняется. Если, например, перенести параллельную активити с множественными экземплярами, у которой активны два экземпляра из пяти, после миграции состояние будет тем же.

Удаление маркера множественного экземпляра

Если целевая активити не имеет множественных экземпляров, достаточно инструкции для внутренней активити. Во время миграции удаляются переменные множественного экземпляра nrOfInstances, nrOfActiveInstances и nrOfCompletedInstances. Число экземпляров внутренней активити сохраняется: если до миграции активны два экземпляра из пяти, после миграции будет два экземпляра целевой активити. Их переменные loopCounter и переменные элемента коллекции тоже сохраняются.

Асинхронные продолжения

Активное асинхронное продолжение, то есть такое, чье задание исполнитель заданий еще не выполнил, представлено экземпляром перехода (transition instance). Так бывает, например, когда выполнение задания завершилось ошибкой и создан инцидент. Для экземпляров перехода инструкции миграции действуют так же, как для экземпляров активности. Если есть инструкция из активити userTask в активити newUserTask, все экземпляры перехода, представляющие асинхронное продолжение до или после userTask, переносятся в newUserTask. Для успешной миграции целевая активити тоже должна быть асинхронной.

Ограничение для asyncAfter

Экземпляр перехода, представляющий асинхронное продолжение после активити, переносится успешно, только если выполняются условия:

  • если у исходной активити нет исходящих потоков управления, у целевой активити не больше одного исходящего потока управления;

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

Семантика выполнения

Ниже описана точная семантика миграции. Прочитайте этот раздел, чтобы полностью понимать эффекты, возможности и ограничения миграции экземпляров процесса.

Процедура миграции

Миграция экземпляра процесса проходит по шагам:

  1. Назначение инструкций миграции экземплярам активности.

  2. Проверка назначения инструкций.

  3. Отмена несопоставленных экземпляров активности и сущностей обработчиков событий.

  4. Миграция сопоставленных экземпляров активности и зависимых от них экземпляров, создание экземпляров новых областей BPMN и обработчиков для новых событий.

Назначение инструкций миграции

На первом шаге инструкции миграции назначаются экземплярам активности переносимого экземпляра процесса.

Проверка назначения инструкций

Логика миграции должна суметь выполнить созданное назначение, и это обеспечивает шаг проверки. В частности, должны выполняться условия:

  • к листовому экземпляру активности (например, пользовательской задачи) применяется ровно одна инструкция;

  • к нелистовому экземпляру активности (например, встроенного подпроцесса) применяется не больше одной инструкции;

  • назначение в целом выполнимо. Подробнее см. в разделе Проверка.

Отмена несопоставленных экземпляров активности и сущностей обработчиков событий

Нелистовые экземпляры активности, к которым не применяется ни одна инструкция миграции, отменяются. Сущности обработчиков событий (например, подписки на события сообщений или задания таймеров) удаляются, если их элементы BPMN (например, граничные события) не переносятся. Отмена выполняется до применения любой инструкции миграции, поэтому экземпляр процесса еще находится в состоянии до миграции.

Семантика:

  • дерево экземпляров активности обходится снизу вверх, несопоставленные экземпляры отменяются;

  • при отмене экземпляра активности вызываются слушатели выполнения активити на событие end и выходные сопоставления переменных.

Миграция и создание экземпляров активности

На последнем шаге экземпляры активности переносятся, а новые при необходимости создаются.

Семантика:

  • дерево экземпляров активности обходится сверху вниз;

  • если экземпляр активности переносится в область BPMN, в которую не переносится ни один родительский экземпляр активности, создается новый экземпляр активности;

  • при создании вызываются слушатели выполнения активити на событие start и входные сопоставления переменных;

  • экземпляр активности переносится по назначенной ему инструкции миграции.

Миграция экземпляра активности

Миграция экземпляра активности обновляет ссылки на активити и определение процесса в дереве экземпляров активности и в его представлении в виде объектов выполнения. Кроме того, переносятся или удаляются зависимые экземпляры, принадлежащие экземпляру активности:

  • экземпляры переменных;

  • экземпляры задач (для пользовательских задач);

  • экземпляры подписок на события.

Проверка

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

Проверка гарантирует, что ограничения, описанные в этом разделе, не приведут после миграции к несогласованному состоянию экземпляра процесса с неопределенным поведением.

Проверка при создании

Чтобы инструкция была корректной при создании плана миграции, она должна удовлетворять требованиям:

  • сопоставляет активити одного типа;

  • задает связь один к одному;

  • перенесенная активити остается потомком ближайшей переносимой родительской области (сохранение иерархии);

  • план миграции учитывает особенности элементов BPMN;

  • устанавливаемая переменная не должна иметь тип Object с форматом сериализации (serializationFormat) application/x-java-serialized-object.

Если проверка находит ошибки, миграция завершается исключением MigrationPlanValidationException с объектом MigrationPlanValidationReport, в котором описаны ошибки проверки.

Сохранение иерархии

Активити должна оставаться потомком ближайшей родительской области, которая переносится (то есть не отменяется во время миграции).

Рассмотрим следующий план миграции для процессов из примера в начале раздела:

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapActivities("assessCreditWorthiness", "handleApplicationReceipt")
  .mapActivities("validateAddress", "validatePostalAddress")
  .build();

И экземпляр процесса в состоянии:

ProcessInstance
└── Assess Credit Worthiness
    └── Validate Address

Этот план миграции применить нельзя, потому что нарушено требование сохранения иерархии: активити Validate Address должна перейти в Validate Postal Address, но родительская область Assess Credit Worthiness переносится в Handle Application Receipt, где нет Validate Postal Address.

Проверка при выполнении

Перед применением плана миграции к экземпляру процесса движок проверяет, что план применим. В частности, проверяются:

  • полнота: для каждого экземпляра листовой активити (то есть активити, не содержащей других активити) должна быть инструкция миграции;

  • применимость инструкций: для некоторых типов активити переносить можно только экземпляры перехода, но не экземпляры активности.

Если проверка находит ошибки, миграция завершается исключением MigratingProcessInstanceValidationException с объектом MigratingProcessInstanceValidationReport, в котором описаны ошибки проверки.

Полнота

Миграция имеет смысл, только если к каждому экземпляру листовой активити применяется инструкция миграции. Пусть план миграции такой:

MigrationPlan migrationPlan = processEngine.getRuntimeService()
  .createMigrationPlan("exampleProcess:1", "exampleProcess:2")
  .mapActivities("archiveApplication", "archiveApplication")
  .build();

Рассмотрим экземпляр процесса в следующем состоянии экземпляров активности:

ProcessInstance
└── Archive Application

Для этого экземпляра процесса план полон: для активити Archive Application есть инструкция миграции.

Теперь рассмотрим другой экземпляр процесса:

ProcessInstance
├── Archive Application
└── Assess Credit Worthiness
    └── Validate Address

Для этого экземпляра план некорректен: к экземпляру Validate Address не применяется ни одна инструкция.

Применимость инструкций

Инструкции миграции переносят как экземпляры активности, так и экземпляры перехода (активные асинхронные продолжения). Некоторые инструкции переносят только экземпляры перехода, но не экземпляры активности. В общем случае переносить можно экземпляры активности только следующих типов элементов:

  • задачи:

    • пользовательская задача;

    • задача получения;

    • внешняя задача;

  • подпроцессы:

    • встроенный подпроцесс;

    • событийный подпроцесс;

    • транзакционный подпроцесс;

    • действие по вызову;

  • шлюзы:

    • параллельный шлюз;

    • включающий шлюз;

    • событийный шлюз;

  • события:

    • граничное событие;

    • промежуточное перехватывающее событие;

  • прочее:

    • тело множественного экземпляра.

Экземпляры перехода можно переносить для любого типа активити.

Что не охватывает проверка

Согласованность данных

Экземпляры процесса содержат данные, например переменные, которые зависят от реализации процесса. Проверка не может гарантировать, что эти данные пригодны в контексте целевого определения процесса.

Десериализация переменных типа Object

Переменные типа Object представляют Java-объекты. У них есть сериализованное значение и имя Java-типа, по которому значение десериализуется в Java-объект. При миграции между процессами разных процессных приложений может оказаться, что переменная типа Object ссылается на Java-класс, которого нет в процессном приложении целевого процесса.

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

  • добавить недостающие классы в целевое процессное приложение;

  • по сериализованному значению преобразовать несогласованную переменную в переменную, Java-класс которой доступен.

Документация создана на основе Camunda 7 Docs (Camunda, docs.camunda.org) и распространяется по лицензии Creative Commons Attribution-ShareAlike 3.0 Unported.