ECOS CMMN

Общие сведения

CMMN + ecos process - движок бизнес процессов.

Движок создан для ускорения и сокращения объемов миграций - definition процесса не хранится в разрезе каждого кейса в Alfresco, а имеет свой datasource (ecos-process микросервис).

В общем виде последовательность шагов бизнес-движка при выполнении каких-либо действий с кейсом (создание/завершение задачи/выполнение пользовательского действия и тд):

  1. В движок поступает событие (Event).

  2. Начинается транзакция в Alfresco.

  3. В процессе выполнения, запрашивается state и/или definition посредством команд (через RabbitMQ) из ecos-process микросервиса.

  4. Выполняются действия по процессу (активация или завершение активностей и тд).

  5. Перед коммитом транзакции, измененный state сохраняется в ecos-process посредством команд, новый stateId сохраняется в ноду.

  6. Транзакция коммитится, фиксируется новый stateId для ноды, исходя из которого будут происходить дальнейшие действия.

Схематично это можно показать в следующем виде:

Колонки

В случае проблем во время транзакции, в том числе после отправки нового state в микросервис - откат транзакции не меняет stateId в ноде кейса и мы не теряя данных, может продолжать работать с кейсом с последней зафиксированной общей в обоих инстансах точки.

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

Интеграция CMMN с ecos-process

Интеграция осуществляется через библиотеку ecos-commands.

Микросервис ecos-process реализует на своей стороне несколько command executor’ов, которыми пользуется CMMN для получения/создания/обновления состояние или поиска конкретного описания процесса.

Команды и их роли, описаны в таблице:

Название команды

Структура запроса

Структура ответа

Описание

create-proc-instance

procType: String
ecosTypeRef: RecordRef
alfTypes: String[]
procDefId: String
procDefRevId: String
Создает новый пустой инстанс процесса (состояние) в микросервисе.
Возвращает идентификатор процесса и идентификатор состояния.
Идентификатор состояния обязателен для обновления состояния в будущем.
Вызывается при создании кейса единоразово.

get-proc-def-rev

procType: String
procDefRevId: String
id: String
format: String
data: byte[]
procDefId: String
version: int
Производит поиск по recisionId описания конкретного процесса.
В data - хранится описание процесса, которое парсится в удобный формат и позже кешируется.

create-proc-instance

procDefRevId: String
recordRef: RecordRef
procId: String
procStateId: String
procStateData: byte[]
Создает новый пустой инстанс процесса (состояние) в микросервисе.
Возвращает идентификатор процесса и идентификатор состояния.
Идентификатор состояния обязателен для обновления состояния в будущем.
Вызывается при создании кейса единоразово.

get-proc-state

procType: String
procStateId: String
procDefRevId: String
stateData: byte[]
version: int
Получение по stateId состояния процесса с данными.
Состояние хранится в том же виде, в котором происходило и сохранение (см. update-proc-state).

update-proc-state

prevProcStateId: String
stateData: byte[]
procStateId: String
version: int
Сохранение в микросервисе нового обновленного состояния.
Возвращает идентификатор новой версии состояния.

Примечание: procType для CMMN всегда имеет значение cmmn.

Результат выполнения команд find-proc-def ``и ``get-proc-def-rev кешируется следующим образом:

Для команды find-proc-def кешируется идентификатор ревизии, который нужен для выполнения следующей команды. Ключом кеша является структура ecosTypeRef+alfTypes. Используется Google Guava кеш.

Для команды get-proc-def-rev дела обстоят несколько сложнее. После выполнения команды, описание процесса парсится в удобный формат для процесса (см описание парсинга в соседней статье) и результат парсинга уже кешируется. Ключом кеша является идентификатор ревизии. Используется Google Guava кеш.

Полностью работу с микросервисом ecos-process берет на себя сервис ru.citeck.ecos.icase.activity.service.eproc.EProcActivityServiceImpl. Так же, он может предоставлять информацию по доп кешам конкретного дефинишена.

Интеграция CMMN с таймерами ecos-process

Микросервис ecos-process позволяет “зашедулить” некоторую команду на выполнение в будущем, в какой-то момент времени.

Последовательность следующая:

  1. Срабатывает активность таймера в CMMN.

  2. CMMN отправляет команду в ecos-process со временем, в которое эту команду нужно вернуть и описанием ответной команды.

  3. Проходит отведенное время, ecos-process отправляет ответную команду в приложение, указанное для ответной команды (в данном случае, Alfresco).

  4. Завершается активность таймера в CMMN.

Команды для управления таймерами со стороны ecos-process:

Название команды

Структура запроса

Структура ответа

Описание

create-timer


{   triggerTime: Instant,
       command: {
       id: String,
       targetApp: String,
       type: String,
       body: ObjectData
       }
}

{
        timerId: String
}
Создает таймер в ecos-process.
После прошествия времени, которое указано в triggerTime, ecos-process составит команду на основании структуры command и отправит ее в targetApp из структуры command.
Микросервис, если в body не было указано поле с названием timerId, то добавит туда настоящий из ecos-process timerId.

cancel-timer


{
        timerId: String
}

{
       wasCancelled: boolean
}
Отменяет таймер в ecos-process по идентификатору.

Новый CMMN реализует executor с типом eproc-timer-occur для реакции на таймеры. Если ошибочно установленный (к примеру, оставшийся после отката транзакции) таймер вернет команду в CMMN, движок не отреагирует эту команду, так как, таймер с таким id не соответствует активности таймера.

В ECOS 3 CaseTimerJob проверяет таймеры раз в 30 минут по умолчанию. Расписание настраивается.

../../_images/timer.png

Парсинг описания для нового CMMN

Основную работу по парсингу выполняет класс ru.citeck.ecos.icase.activity.service.eproc.importer.parser.CmmnSchemaParser.

Парсинг состоит из двух стадий:

С помощью JAXB, парсит definition в структуры старого CMMN.

Структуры старого CMMN парсит в единый объект ProcessDefinition’а с вложенными структурами активностей разных типов, с описаниями переходов и тд.

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

public class OptimizedProcessDefinition {
       private Definitions xmlProcessDefinition;
       private ProcessDefinition processDefinition;
       private Map<String, ActivityDefinition> idToActivityCache;
       private Map<String, SentryDefinition> idToSentryCache;
       private Map<SentrySearchKey, List<SentryDefinition>> sentrySearchCache;
       private Map<String, Set<ActivityDefinition>> roleVarNameToTaskDefinitionCache;
}

где:

  • xmpProcessDefinition - результат первой парсинга JAXB (первой стадии парсинга). Обязателен для импорта ролей и элементов кейса.

  • processDefinition - неоптимизированное описание процесса, результат второй стадии парсинга.

  • idToActivityCache - кеш ActivityDefinition’ов по идентификаторам.

  • idToSentryCache - кеш SentryDefinition’ов по идентификаторам.

  • sentrySearchCache - кеш для поиска SentryDefinition’ов, которые могут сработать при прошествии события, описанного в SentrySearchKey. Смысл этого кеша в том, чтоб без перебора всего процесса найти те sentry, которые могут произойти при событии какого-то типа для определенного SourceRef. В дальнейшем, будут выполнены для этих sentry их evaluator’ы и только те что вернули true - сработают sentry. Активности, привязанные к этим sentry триггерами - перейдут в новое состояние согласно описанным переходам. SentrySearchKey состоит из SourceRef+EventType.

  • roleVarNameToTaskDefinitionCache - кеш названий ролей к ActivityDefinition с типом “пользовательская задача”. Используется для синхронизации изменившихся ролей с запущенными задачами.

Импорт кейса

Для импорта кейсов - давно существует бихейвиор ru.citeck.ecos.behavior.CaseTemplateBehavior.

Процесс импорта новой реализации CMMN

Процесс импорта можно расписать по следующим шагам:

  1. Определение в бихейвиоре, импортировать ли CMMN кейс? Если да, то продолжаем.

  2. Парсинг ProcessDefinition, расписанного в соседней статье.

  3. Импорт ролей.

  4. Импорт элементов кейса.

  5. Создание нового состояния в микросервисе (с помощью команды create-proc-instance, описанной в соседней статье).

  6. Сохранение stateId и processId в ноду кейса.

  7. Создание ProcessInstance исходя из ProcessDefinition перебором активностей. ProcessInstance сохраняется в транзакции.

  8. Отправка события case-created по процессу.

Как включить новую реализацию CMMN

Первое на что смотрится, какая реализация вообще включена в системном журнале конфигурации по ключу ecos-case-process-type. Может быть 2 значения:

  • alf - Всегда выбирать alfresco реализацию CMMN.

  • eproc - Выбирать ecos-process реализацию CMMN при условии, что для этого типа включена новая реализация. Иначе - выбирать alfresco реализацию CMMN.

Как включить eproc реализацию для конкретного типа при условии, что eproc реализация включена в системном журнале

Класс ru.citeck.ecos.icase.activity.service.eproc.importer.EProcCaseImporter имеет список типов, доступных для новой реализации CMMN.

Чтобы зарегистрировать новый тип - можно создать бин класса ru.citeck.ecos.icase.activity.service.eproc.importer.EProcTypeRegistrar.

Пример для коробочных договоров:

<bean id="contracts.eproc.registrarForEnabled" class="ru.citeck.ecos.icase.activity.service.eproc.importer.EProcTypeRegistrar">
       <property name="alfTypes">
              <list>
                     <value>{http://www.citeck.ru/model/contracts/1.0}agreement</value>
              </list>
       </property>
</bean>

Поддерживает наследование типов alfresco, то есть, если указать sys:base тип - то eproc реализация будет доступна для всех типов (при условии, что в журнале eproc реализация включена).

Поддерживает указание не только alfresco типов, но и ecosType (RecordRef).

Как работает движок

Триггером для начала обработки внутри процесса всегда является событие (Event).

Чтобы началась обработка конкретного Event, нужно, чтобы произошла какая-либо из активностей следующих возможных типов (eventType):

  • activity-started - срабатывает при запуске активности (внутреннее событие процесса).

  • activity-stopped - срабатывает при завершении активности (внутреннее событие процесса).

  • stage-children-stopped - срабатывает при завершении дочерней активности, при условии, что все дочерние элементы не активны (внутреннее событие).

  • case-created - срабатывает единоразово при создании кейса (внешнее событие).

  • case-properties-changed - срабатывает при изменении свойств процесса (внешнее событие, инициатор - бихейвиор).

  • user-action - срабатывает при выполнении действия пользователем из виджета действий (внешнее событие).

После срабатывания, например, “activity-started” eventType для активности с id=”id-2” - начнется поиск из кешей Sentry с подходящими параметрами.

Найденные Sentry будут проверены evaluator’ами и уже для тех, что были пропущены evaluator’ами - будет запущена обработка события.

Сработавшее событие смотрит из своей Sentry, к чему она привязана по следующей схеме (некоторые переходы по сущностям опущены для поддержания простоты усвоения последовательности шагов):

Колонки

где процесс из ActivityTransitionDefinition(1) смотрит toState. В зависимости от его значения:

  • toState=Started (fromState=Not started) - В данном случае будет запущена активность процесса с привязанной definition(2) текущего процесса. Если активность уже была запущена и она перезапускаема - будет произведен reset перед запуском активности.

  • toState=Completed (fromState=Started) - В данном случае будет остановлена активность процесса с привязанной definition(2) текущего процесса.

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

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

Основные классы для управления активностями и триггирования событий:

  • ru.citeck.ecos.icase.activity.service.eproc.EProcCaseActivityDelegate

  • ru.citeck.ecos.icase.activity.service.eproc.EProcCaseActivityEventDelegate

Как работают таймеры

При запуске активности с типом “Таймер” - происходит отправка команды в микросервис ecos-process на создание таймера.

После того, как таймер дотикает - происходит обратная команда из ecos-process в альфреско, которая останавливает активность таймера. Остановка активности таймера начинает триггерить смену состояний других активностей, завязанных на него и процесс идет дальше.

Evaluator’ы для событий

После того, как sentry был найден, нужно определить, нужно ли триггировать данное событие.

За это ответственны evaluator’ы, описание которых можно найти в SentryDefinition сущности.

В новой реализации CMMN, при триггировании события, конвертируются EvaluatorDefinition в понятные для RecordEvaluatorService структуры вида RecordEvaluatorDto и скармливаются, непосредственно, сервису RecordEvaluatorService.

Если сервис вернул true - значит, событие происходит. Иначе - игнорируется.

Конвертация EvaluatorDefinition в RecordEvaluatorDto происходит в классе ru.citeck.ecos.icase.activity.service.eproc.EProcCaseEvaluatorConverter. Маппинг доступных эвалюаторов можно посмотреть там же.

Command’ы для действий

Команды происходят синхронно и на локальном инстансе (Alfresco).

Всю работу делает класс ru.citeck.ecos.icase.commands.CaseCommandsServiceImpl.

Алгоритм примерно следующий:

  1. В старой или новой реализации CMMN запускается активность с типом Action. Каждый движок своими средствами обращается к CaseCommandsService с идентификатором активности.

  2. CaseCommandsService вытягивает из активности тип события.

  3. По типу события ищет зарегистрированный Provider команд.

  4. Собирает с помощью провайдера команду.

  5. Отправляет команду на выполнение.

Список существующих команд:

  • Выполнить скрипт (ru.citeck.ecos.icase.commands.executors.ExecuteScriptCommandExecutor);

  • Fail, просто выбрасывает ошибку. Используется с каким-нибудь Evaluator’ом (ru.citeck.ecos.icase.commands.executors.FailCommandExecutor);

  • Сигнал БП (ru.citeck.ecos.icase.commands.executors.SendWorkflowSignalCommandExecutor);

  • Установить статус кейса (ru.citeck.ecos.icase.commands.executors.SetCaseStatusCommandExecutor);

  • Установить переменную процесса (ru.citeck.ecos.icase.commands.executors.SetProcessVariableCommandExecutor);

  • Установить переменную кейса (ru.citeck.ecos.icase.commands.executors.SetPropertyValueCommandExecutor).

Полезные JS-скрипты при работе с CMMN + ecos-process

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

var document = search.findNode('workspace://SpacesStore/2523f47a-f9aa-4320-81d7-6551c2e42fcc');
 getActivities(document, 0);

 function getActivities(parent, level) {
         var activities = CaseActivityService.getActivities(parent);
         for (var i in activities) {
                 var activity = activities[i];
                 printActivity(activity, level);
                 var childActivities = CaseActivityService.getActivities(activity);
                 getActivities(activity, level + 1);
         }
 }

 function printActivity(activity, level) {
         var spaces = '';
         for (var i = 0; i < level; i++) {
                 spaces = spaces + '    ';
         }
         print(spaces + activity.title + " : " + activity.state); // Тут можно расширить вывод другими данными из сущности CaseActivity.
 }

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

var srv = services.get('eprocActivityService');

 var cache1 = Packages.org.apache.commons.lang.reflect.FieldUtils.readField(srv, 'typesToRevisionIdCache', true);
 cache1.invalidateAll();

 var cache2 = Packages.org.apache.commons.lang.reflect.FieldUtils.readField(srv, 'revisionIdToProcessDefinitionCache', true);
 cache2.invalidateAll();

Проверка наличия шаблона для типа в обход кэшей:

var srv = services.get('eprocActivityService');
var commandsService = Packages.org.apache.commons.lang.reflect.FieldUtils.readField(srv, 'commandsService', true);
var findProcDefCommand = new Packages.ru.citeck.ecos.icase.activity.service.eproc.commands.dto.request.FindProcDef();
findProcDefCommand.setProcType("cmmn");
findProcDefCommand.setEcosTypeRef(Packages.ru.citeck.ecos.records2.RecordRef.valueOf("emodel/type@supplementary-agreement/6ce21c23-d1e7-43b3-994b-2c3c305d320d"));
print(commandsService.executeSync(findProcDefCommand, "eproc"));

Проверка наличия шаблона для заявки с учетом кэшей:

var srv = services.get('eprocActivityService');
print(srv.getFullDefinition(Packages.ru.citeck.ecos.records2.RecordRef.valueOf("workspace://SpacesStore/45a5d9cf-502f-4622-bf41-040df6d599e5")));

Повторное применение шаблона кейса к документу без статуса или в статусе “Error while starting the process/ОШИБКА ПРИ СТАРТЕ ПРОЦЕССА“ (ecos-process-start-error):

var document = search.findNode("workspace://SpacesStore/***");
if (document.hasAspect("req:hasCompletenessLevels")) {
        document.removeAspect("req:hasCompletenessLevels");
}
services.get('caseTemplateBehavior').onAddAspect(document.nodeRef, citeckUtils.createQName('icase:case'));

Сброс и перезапуск EPROC процесса кейса

var document = search.findNode('workspace://SpacesStore/6bb46ade-b5d0-4c0b-bca6-71298e6979a7');
CaseActivityService.reset(document);
caseActivityEventService.fireEvent(document, 'case-created');

Переприменение шаблона кейса (запустить процесс заново с последней версией шаблона)

Выполнение в 2 этапа
>>>>> 1. Сбрасываем состояние кейса и кэш шаблонов

var document = search.findNode('workspace://SpacesStore/6558016c-e787-4f24-9d43-34d2739f01a2');

var srv = services.get('eprocActivityService');
var cache1 = Packages.org.apache.commons.lang.reflect.FieldUtils.readField(srv, 'typesToRevisionIdCache', true);
cache1.invalidateAll();
var cache2 = Packages.org.apache.commons.lang.reflect.FieldUtils.readField(srv, 'revisionIdToProcessDefinitionCache', true);
cache2.invalidateAll();

CaseActivityService.reset(document);

>>>>> 2. Сбрасываем для кейса шаблон и состояние, запускаем импорт и стартуем новый процесс:

document.properties['icaseEproc:stateId'] = null;
document.properties['icaseEproc:definitionRevisionId'] = null;

document.save();

var roles = document.childAssocs['icaseRole:roles'];
for each(var role in roles) {
        role.remove();
}
services.get('EProcCaseImporter').importCase(Packages.ru.citeck.ecos.records2.RecordRef.valueOf("" + document.nodeRef))
caseActivityEventService.fireEvent(document, 'case-created');