FAQ

Вопросы и ответы

Логирование процесса (задача-сценарий или script task)

Формат скрипта JavaScript

В поле 'Script' объявить и использовать логгер или использовать стандартную функцию вывода println

var logger = java.util.logging.Logger;
var log = logger.getLogger('MY_JS_LOGGER');
log.info('Im logging INFO');

Формат скрипта Groovy

import org.slf4j.*
 
Logger logger = LoggerFactory.getLogger('MyScriptLogger');
logger.info('Hello World');

Способы анализа ошибок исполнения процесса

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

Способ избегания ситуаций, когда сложно выявить ошибку - установка дополнительных точек точек ожидания.

Пример с ошибкой в условии на шлюзе

empty

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

empty

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

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

Лог для этого кейса

В конце данного лога можно встретить следующую ошибку (полное описание ошибки и stack trace можно посмотреть в файле лога):

ERROR: condition expression returns non-Boolean: result has class java.lang.String and not java.lang.Boolean

Эта ошибка говорит о том, что Условное выражение возвращает результат с типом "Строка", а должен быть тип "Булевый" (true/false). Подобная ошибка может возникнуть в местах где используются условные выражения - на ветках/соединителях или на элементе: промежуточное событие-условие.

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

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

Диаграмма процесса

Исполнение процесса зависло на кубике получения сообщения из кафки

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

Ошибка при запуске экземпляра процесса

Вариант 1

При запуске экземпляра бизнес-процесса (вызов метода POST) можно получить код ответа HTTP 500. Можно распарсить параметр errorMessage или посмотреть в логи МС. В указанном параметре будет содержаться сообщение вида:

Internal exception, please see server log, message: Output topic «input-topic» not registered

Данная ошибка говорит о том, что в микросервисе исполнения бизнес-процесса не зарегистрирован топик "input-topic" для отправки (output) сообщений. Аналогичная ошибка может быть и для топиков-подписок.

Лог для этого кейса

Вариант 2

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

Условия, которые привели к ошибке:

  • при первой публикации бизнес-процесса, имя и ID пула процесса остаются прежними;
  • при последующем изменении версии диаграммы и публикации изменить наименование и ID пула процесса.
ENGINE-13030 Cannot correlate a message with name 'qhldrq-vacationrequest-command-processstart' to a single process definition. 2 process definitions match the correlations keys: CorrelationSet [businessKey=null, processInstanceId=null, processDefinitionId=null, correlationKeys=null, localCorrelationKeys=null, tenantId=null, isTenantIdSet=false, isExecutionsOnly=false]

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

Данную ошибку можно поправить, но лучше всего устранить причину, которая к этому привела.

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

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

  1. Необходимо в БД модуля "Дизайнер процессов" в таблице diagram_item найти все записи, относящиеся к пулу процесса.
select *
from qbpmdesigner.q_bpm_diagram_item qbdi
where diagram_version_id in (
    select id
    from qbpmdesigner.q_bpm_diagram_version qbdv
    where diagram_id = <идентификатор записи диаграммы>
)
and type = 'process'
  1. По колонке system_name определить системное имя неактуального пула процесса.
  2. Если записей по неактуальному пулу процесса несколько, то запомнить все значения из колонки diagram_version_id - это и есть "плохая" версия процесса.
  3. Удалить все существующие связи во всех таблицах БД модуля "Дизайнер процессов", в том числе в таблицах act_* тоже (системные таблицы камунды).

После всех действий ошибка будет считать устраненной.

Корреляция сообщений

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

Сообщение не отправляется напрямую экземпляру. Корреляция сообщений основана на подписках, которые содержатmessage name и correlation key.

Возможна реализация двух подходов (первый работает по умолчанию).

  1. Автоматический Все настройки у МС остаются неизменными, ключ корреляции генерируется автоматически и помещается в параметр correlationId.

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

🚫

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

  1. Ручной

    Ключ корреляции можно сгенерировать/задать самостоятельно при проектировании процесса. Для этого в настройках сервиса qbpetqbpmplayer необходимо в параметре ${QBPM_CORRELATION_KEYS:dqMessageGuid} задать название поля для ключа корреляции.

    При формировании отправки сообщения в kafka указанный параметр с ключом корреляции будет размещен в теле и заголовке сообщения.

Зарезервированные события платформы

Наименование заголовка сообщенияОписание событияТело сообщенияИнициатор -> Принимающий
dq-q-bpm-task-eventСобытие на изменение пользовательской задачи (создание, обновление, закрытие) - передача данных об исполнителеHuman task → Player
taskActionEventTopicCобытие о действиях по задаче, таски передают информацию в плеер.Human task → Player
taskCreateEventTopicCобытие на создание задачиPlayer → Human task
taskCancelEventTopicCобытие на отмену задачи в случае terminate (убийства) процессаHuman task → Player
dq-q-bpm-decision-definitionСобытие с описанием конкретного правила при публикацииPlayer → Designer
dq-q-bpm-decision-requirements-definition"Событие с описанием DMN диаграммы (дерева принятия решений) при публикацииPlayer → Designer
dq-q-bpm-decision-evaluationСобытие об исполненном бизнес-правилеPlayer → Designer
dq-q-bpm-diagram-deploy-requestCобытие о публикации от дизайнера к плееруDesigner → Player
dq-q-bpm-diagram-deploy-responseCобытие о факте публикации от плеера к дизайнеруPlayer → Designer
dq-q-bpm-diagram-process-definitionCобытие от плеера к дизайнеру, при котором происходит выделение пулов процессов в диаграмме и связываются пулы с диаграммойPlayer → Designer
dq-q-bpm-diagram-versionСобытие от дизайнера в кафку при ручном сохранении (кнопка "Дискета" в редакторе) диаграммыDesigner → All
dq-q-bpm-process-instance-protocolПротокол по исполнению экземпляра процесса. По старту и по завершению.Player → Cockpit
dq-q-bpm-process-activity-protocolПротокол по исполнению экземпляра шага процесса. По старту и по завершению.Player → Cockpit
dsOnAfterProcessDefinitionCreateCобытие о появлении процесса (плеер шлет в кукпит о публикации)Player → Cockpit
dsOnProcessActivityEventCобытие на каждый узел процесса в диаграмме от плеераPlayer → Cockpit
dsOnProcessInstanceEventКогда появляется инстанс процесса и завершается. Старт процесса status = ACTIVE и завершение процесса status = COMPLITED.Player → Cockpit
dq-q-bpm-incidentСобытие об упавшем процессе, который приостановлен для вмешательства пользователяPlayer → Cockpit

Преобразование типов данных в скриптах бизнес-процесса на JavaScript

Контекст бизнес-процесса хранит данные в переменных с типами Java-объектов. Для обработки объектов функциями JavaScript необходимо произвести преобразование Java-объектов получаемых из контекста процесса в объекты JavaScript. Для данного преобразования в скриптовых узлах процесса необходимо использовать методы объекта ObjectMapper.

writeValueAsString( variable <object> ) // преобразование объекта к строке
readValue(someVariableAsStringOutput, java.util.LinkedHashMap.class) // преобразование строковой переменной к java-объекту указанного типа (в данном случае LinkedHashMap)

Для работы в скрипте узла бизнес-процесса с объектами JavaScript необходимо предварительно произвести преобразование строковой переменной к объекту JavaScript:

JSON.parse(someVariableAsString)

Для преобразования JavaScript объекта к строке необходимо использовать метод:

JSON.stringify(someVariableAsObject)

Последовательность необходимых преобразований в скрипте процесса для получения Java-объекта из контекста процесса, обработки данных как JavaScript объекта и передачи обратно в контекст процесса в виде Java-объекта:

// получение переменной из контекста процесса
var someVariable = execution.getVariable("someVariable");
 
// преобразование полученного объекта к json-строке
var someVariableAsString = objectMapper.writeValueAsString(someVariable);
 
// преобразование json-строки к JavaScript объекту
var someVariableAsObject = JSON.parse(someVariableAsString);
 
//
// выполнение логики скрипта с JavaScript объектом
//
 
// преобразование JavaScript объекта к строке
var someVariableAsStringOutput = JSON.stringify(someVariableAsObject);
 
// преобразование строки в Java-объект (java.util.LinkedHashMap)
var someVariableAsMapOutput = objectMapper.readValue(someVariableAsStringOutput, java.util.LinkedHashMap.class);
 
// передача переменной в контекст процесса
execution.setVariable("someVariable", someVariableAsMapOutput);

Создание виджета и использование в шаблоне пользовательской задачи

Для создания виджета необходимо использовать инструкцию (opens in a new tab).

Режимы взаимодействия виджета и основной формы обработки задачи

  1. Управление действиями по обработке задаче осуществляется на основной форме.

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

    Для отображения на форме информации, переданной из процесса в пользовательскую задачу, необходимо добавить в код виджета свойство data(@Input() data: Task = {};). В этом случае, в объекте data будет доступны все данные по задаче и параметры переданные из процесса bpmn.

  2. Управление действиями на стороне виджета

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

    Для этого в виджет необходимо добавить:

    • свойство actions;
    • события taskActionTriggered, widgetInfo, cancel;
    • в ngOnInit (момент инициализации виджета), необходимо прокинуть событие в widgetInfo, для уведомления основной форме о режиме работы текущего виджета:
      ngOnInit
      @Input() actions: MenuItem[];
       
      @Output() widgetInfo = new EventEmitter<TaskProcessingWidgetInfo>();
      @Output() taskActionTriggered = new EventEmitter<TaskProcessingResultAction>();
      @Output() cancel = new EventEmitter<string>();
       
      ngOnInit(): void {
          this.widgetInfo.emit({
          usesActionButtons: true // true - управляющие кнопки реализуются на стороне виджета, false - управляющие кнопки должны предоставляться основной формой обработки задач
          });
      }

    Виджет может инициировать событие о завершении обработки задачи пользователем и передачи управления основной форме, которая переведёт пользовательскую задачу в состояние "Выполнена". Для этого необходимо пробросить событие в taskActionTriggered.

    onActionButtonClicked(): void {
        this.taskActionTriggered.emit({
            id: 'DONE', // Поле id из выбранного элемента actions. Все что не входит в actions игнорируется так как такое действие не может быть обработано в процессе bpmn
            comment: 'Some commet' // Комментарий пользователя или дополнительная информация (поле необязательное)
        });
    }

    Также виджет может инициировать событие об отмене обработки пользовательской задачи (в этом случае форма обработки будет закрыта без выполнения каких либо действий на задаче). Для этого необходимо пробросить событие в cancel.

    onCancel(): void {
        this.cancel.emit();
    }

    Для обеспечения взаимодействия виджета обработки задачи с общей формы обработки задач, виджет должен обеспечивать контракт предоставляемый интерфейсом TaskProcessingWidgetComponent. Для это необходимо подключить библиотеку npm i @diasoft/q-bpm-human-task-widget или в package.json добавить @diasoft/q-bpm-human-task-widget": "0.0.9.

    export interface TaskProcessingWidgetComponent {
        actions: MenuItem[]; // набор или список действий, которые будут доступны для обработки задачи
        data: Task; // данные обрабатываемой задачи        
        taskActionTriggered: EventEmitter<TaskProcessingResultAction>; // событие, которое отлавливается общей формой обработки задачи. Это событие должно быть инициировано в момент обработки задачи по действию пользователя
    }

Пример реализации виджета

Пример кода виджета для обработки задачи
import {ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {FormBuilder, FormGroup} from '@angular/forms';
import {MenuItem} from 'primeng/api';
import {TaskProcessingAction} from './task-processing-action.model';
import {
    Task,
    TaskProcessingResultAction,
    TaskProcessingWidgetComponent,
    TaskProcessingWidgetInfo
} from '@diasoft/q-bpm-human-task-widget';  // Подключение библиотеки
 
@Component({
    selector: 'app-task-processing-widget',
    templateUrl: './task-processing-default-widget.component.html',
    styleUrls: ['./task-processing-default-widget.component.scss']
})
export class TaskProcessingDefaultWidgetComponent implements TaskProcessingWidgetComponent, OnInit {
    private readonly defaultActions: TaskProcessingAction[] = [
    {label: 'Обработать', id: 'NEXT'}
    ];
 
    // имплементируем компонент
 
    private actionsInner: TaskProcessingAction[] = this.defaultActions;
 
    @Input() set actions(actions: any) {
        this.actionsInner = actions || [];
        this.actionButtonItems = this.getActionButtonItems();
 
        if (actions.length > 0) {
            this.taskProcessingAction = this.mapActionToSplitButtonItem(actions[0]);
        }
    }; // получаем действия по задаче
 
    @Input() data: Task = {}; // получаем остальную информацию по задаче для работы логики внутри виджета
 
    @Output() taskActionTriggered = new EventEmitter<TaskProcessingResultAction>(); // событие отправки
    @Output() cancel = new EventEmitter<string>();
    @Output() widgetInfo = new EventEmitter<TaskProcessingWidgetInfo>();
 
    actionButtonItems: MenuItem[] = this.getActionButtonItems();
    taskProcessingAction: MenuItem = this.mapActionToSplitButtonItem(this.defaultActions[0]);
 
    form: FormGroup = this.fb.group({
        comment: ['']
    });
 
    showActionsButtons = true;
 
    constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {
    }
 
    ngOnInit(): void {
        this.widgetInfo.emit({
            usesActionButtons: this.showActionsButtons
        });
    }
 
    getActionButtonItems(): MenuItem[] {
        return this.actionsInner.map(action => this.mapActionToSplitButtonItem(action));
    }
 
    mapActionToSplitButtonItem(action: TaskProcessingAction): MenuItem {
        return ({
            label: action.label,
            id: action.id,
            command: () => this.onActionSelected(action.id)
        });
    }
 
    onCancel(): void {
        this.cancel.emit();
    }
 
    onActionSelected(actionId: string): void {
    const action = this.actionsInner.find(act => act.id === actionId);
 
        if (action) {
            this.taskProcessingAction = this.mapActionToSplitButtonItem(action);
        }
    }
 
    onActionButtonClicked(): void {
        if (this.taskProcessingAction?.id) {
            this.taskActionTriggered.emit({
                id: this.taskProcessingAction.id,
                comment: this.form.controls.comment.value
            });
        }
    }
}
Интерфейс объекта data
export interface Task {
    id?: any;
    priority?: number; // приоритет задачи
    templateSysName?: string; //сиснейм шаблона
    templateName?: string; //имя шаблона
    objectType?: string;
    version?: number; //версия
    maxProcessTime?: number; // максимальная дата обработки задачи
    maxExecutionTime?: number; // максимальное время исполнения задачи
    taskForm?: string;
    assignAlgorithm?: string; // алгоритм распределения
    state?: string; //состояние
    stateName?: string;
    createDate?: Date;
    endDate?: Date;
    assignDate?: Date; // дата назначения
    assignGroupType?: string;
    assignGroupName?: string;
    assignId?: number;
    assignName?: string;
    assigneeId?: number;
    details?: TaskDetails; // параметры задачи из контекста процесса BPM
    activityName?: string;
    activityId?: string;
    processInstanceId?: string;
    processDefinitionId?: string;
    processDefinitionKey?: string;
    processDefinitionName?: string;
    processDefinitionVersion?: string;
    comment?: string; // коментарий по задаче
    serviceName?: string; // имя сервиса инициатора задачи
}

Использование виджета в шаблоне пользовательской задачи

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

В поле "Виджет обработки задачи" необходимо указать адрес компонента виджета в формате <имя ui-сервиса>:<имя компонента>:<имя виджета>. empty