Русский
Русский
English
Статистика
Реклама

Workflow

Хорошие BPM инструменты, которых нет и нет. Моделирование процессов

12.03.2021 20:06:18 | Автор: admin

Поговорим о том, какие инструменты хотелось бы иметь при описании бизнес-процессов. Инструментов BPMS (BPM systems) много, но выбрать то особо нечего

Ниже перечислим некоторые важные инструментальные возможности некоторых сред моделирования процессов (в основном АРИС-ARIS и MS visio).

Уточнения. BPM (business process management, управление бизнес-процессами) - это тот, который из области системной инженерии (SE), который почему-то теперь называют BPA (анализ). Он же CASE, где S= "system" (не "software").

"Бизнес-процесс" - это синоним "процесс" и даже таким терминам как: операция, действие, активность, функция, процедура. Приставку "бизнес" указывают, чтобы отличать процессы класса "административные" - "деловые" (см. BPM CBOK) от химических и физических (других "природных") процессов. Любой процесс, который реализуется человеком или компьютером (программой, роботом), - это "бизнес-процесс", несмотря на то, что непосредственно к "бизнесу" отношения может и не иметь. Настолько вот запутан этот BPM, хотя в реальности он прост.

Задача

Она очень простая. Нужно простым образом формализовывать процессы, нас окружающие. Так формализовать, чтобы модели процессов были адекватны реальным процессам, но чтобы их визуализацию хоть как-то понимало большинство людей, первый раз слышащих слово "BPM". Формально "интуитивно понятных" BPM-нотаций - много (также как много рекламно-маркетингового шума о BPM), но взять особо нечего. Однако здесь важна не только сама нотация (IDEF\VAD\EPC\BPMN\UML и т.п.), а механизмы ее представления на экране: слои, вариативность "точек зрения" (view-шек, представлений) и т.п. На мой взгляд, лучшим вариантом пока остается EPC (Event-driven Process Chain), но не суть, - представленные ниже подходы могут применяться к другим нотациям.

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

1. Подходы к визуализации диаграммы

1.1 Слои модели

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

Нарисовал нам наш архитектор (специалист по моделированию процессов) схему:

Рис. 1 Процесс оформления заявления

Visio Stencil Library for EPC - не нашел, поэтому "как то так" (штатная EPC - вообще "никакая").

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

Путь к пониманию - это упрощение схемы, например, будем отключать слои. В правильном инструменте (BPM-Tool) должны быть кнопки управления слоями - категориями. По кнопке "отключить ресурсы" - будет скрыт слой "ресурсы", в котором показаны объекты схемы (модели) типа "Роль" {Работник; Начальник} и Инструмент {MS Word}. Уже схема стала менее нагруженной (правой части не стало).

Далее отключаем слой "Документооборот" (docflow) и остается только последовательность действий (workflow, Process Flow), который говорит, что нужно провести всего две операции.

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

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

Пример такой реализации возможен в MS Visio:

Рис. 2 Управление слоями в MS Visio

Инструмент управления слоями, как управление сложностью - давно норма в векторных графических редакторах, ГИС и других CAD-системах, например, AutoCAD.

1.2 Плавательные дорожки

Swimlane позволяют группировать процесс по разрезам "Исполнители" и "Инструменты" (в общем случае - в разрезе любой иной категории объектов).

Применительно к Рис. 1 "Процесс оформления заявления": отключили слой "документы", а оставшуюся часть (функции и ресурсы) представили в виде одной или двух Swimlane (опять же "по кнопке").

Рис. 3 Swimlane по ролям в горизонтальной плоскости

Применительно к рассматриваемому случаю возможны следующие комбинации Swimlane:

- две одинарные (горизонт, вертикаль) по ролям;

- две одинарные (горизонт, вертикаль) по инструментам (часто в разрезе баз данных показывают);

- две двойные, "шахматка", таблица (горизонт - роли, вертикаль - инструменты и наоборот).

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

Основная проблема "дорожек" в том, что когда "дорожки" не нагруженные, то "съедается" основная часть листа и плотность "упаковки" объектов в модели процесса становится мизерная (КПД бассейна, где загружены только две дорожки, а остальные пустые - низкое).

1.3 Объекты модели и их атрибуты, свойства

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

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

В Visio это могут быть данные фигуры и таблица свойств фигуры (ShapeSheet). Еще интереснее свойства хранить в отдельном файле Excel , например, связанном с visio (штатная функция visio). Такой подход позволяет иметь репозитарий свойств объектов в файле Excel и соответственно обширные инструменты поиска, сортировки и т.п. Любой BPM инструмент, включая АРИС, не имеет таких развитых возможностей для анализа как Excel , поэтому выгрузка в Excel интересуемой пользователя атрибутики была бы важным элементом любого BPM-инструмента.

При просмотре схемы процесса должна быть возможность выделения объекта и просмотр атрибутов схемы и ее объектов (с наложением фильтров, т.к. иначе будет избыток).

1.4 Задание своей нотации (на примере новой ЕРС ver. 2)

Посмотрим на примере нотации ЕРС. Что же в ней улучшить? Все улучшения запишем в гипотетическую ЕРС2 нотацию.

Есть предубеждение, что Событийная цепочка процессов обязательно должна иметь строгое чередование "функция" - "событие". Это не так: события указываются только тогда, когда это действительно нужно.

Вообще, от workflow (схема алгоритма) ЕРС отличается в основном двумя параметрами: наличие указания ресурсов и документов (материалов) и иное задание условия разветвления алгоритма (ветвление по условию). Использование элемента "событие" - как указание результата, вместо "да" и "нет" - более функциональное и позволяет кроме того сократить номенклатуру графических примитивов. Событие - как "что-то произошло" и событие - как результат проверки условия.

Как показано в п. 1.1 "Слои модели": выделяем зону docflow, EPC-workflow и ресурсную зону. Docflow, а также любые другие входы и выходы функции (включая материалы, заготовки, полуфабрикаты и конечные продукты) - отображаем слева от функции (отдельная стрелка для всех входов, отдельная для всех выходов) с соответствующим направлением движения, а все ресурсы - справа от функции (без направленных коннекторов). Это позволит иметь стандартный "взгляд" на процесс и сразу фокусироваться на конкретной зоне.

В ЕРС2 будет классификация моделей: приведенная и мультиресурсная. В приведенной схеме будет к каждой функции привязано не более одной роли (инструмента), чтобы была однозначность по исполнителю (инструментарию), что важно не только для анализа, но и при построении Swimlane (каждому "пловцу - исполнителю" по выделенной дорожке).

Возможность задания своей нотации в инструменте моделирования означает подсказку (блокировку) при некорректном построении модели, как в момент отрисовки, так и через проверочный отчет построенной диаграммы. Например, в ЕРС2 предусматриваются следующие типы коннекторов: для входящих сущностей (входящие документы, материалы-заготовки), для выходящих (исходящие документы, продукты операции), соединитель потока (функции, события), соединитель ресурса. В объекте "функция" предусматриваются три "Connection Point" (visio):

- вверху и внизу объекта "функция" (и "события") - для указания структуры потока (очередность действий, событий);

- слева в овале "функция" два коннектора: один вход, второй выход (общие для docflow и потока материалов и т.п.);

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

Такой подход позволит легче читать схему ЕРС, сегодня же "глаза разбегаются", когда видишь паутину и вперемешку разбросанные а) "роли" - и слева и справа функции и б) "документы" - и слева и справа функции.

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

Вопрос: кроме как в visio, где можно задавать новые нотации и делать проверки на соответствие (валидность), аналогичные показанным выше?

1.5 Из таблицы - схему, а из схемы - таблицу

Если посмотреть на ЕРС схему (рис. 1), то видно, что она однозначно задается таблицей. Поля таблицы: вход, выход, функция, исполнитель, инструмент. Заполнили табличку, нажали кнопку "построить" - и схема сгенерировалась. Справедливо и обратное: по нарисованной схеме можно построить адекватную табличку без потери информации (lossless).

Механизм "Из таблицы - схему" в ARIS \ ARIS Express называется Smart Designer. Только он не умеет строить ветвление процесса. На всякий случай: поиск по "ARIS Smart Designer EPC", закладка "картинки".

Преимущества: человек, вообще не знающий нотации (ни одной), по простым правилам заполнил табличку и сгенерировал "по кнопке" схему процесса. Более того, возможно итерационно: вначале заполнил поле "функция" (оно же и "событие", - только с указанием соответствующего признака) - увидел "ствол" функций и событий (магистраль потока, workflow), а потом начал наращивать их левые (вход\выход) и правые (ресурсные) связи.

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

Концептуально изложенный подход близок к выделенной в АРИС нотации "табличная ЕРС" (см. "Нотация ЕРС в виде таблицы"), но здесь реализация в виде обычной текстовой таблицы, т.е. ближе к ARIS Smart Designer. Причем логику процесса также можно указать в составе таблицы, например, как ссылка на предшествующий объект (этого нет Smart Designer, но не сложно добавить "что-то" для ЕРС2). Таблицу можно вставлять в текстовые регламенты word и макросом (VBA) генерить схему процесса ("не отходя от кассы") с дублированием конечно в общем каталоге моделей.

В теме автоматического создания диаграмм из таблицы (особенно Excel) нельзя обойти MS Visio Data Visualizer. Как обычно, - идея "на отлично" (идея далеко не новая), но реализация Видимо в погоне за максимальным универсализмом "выплеснулся ребенок BPM". Я ожидал увидеть что-то такое же простое, функциональное (мощное) и BPM-ориентированное как ARIS Smart Designer. Может это впечатление сложилось из-за отсутствия мастера автопостроения EPC. Кроме того, исключительная модель по подписке не позволяет популяризацию инструмента.

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

1.6 Из скрипта - схему, а из схемы - скрипт

Подход аналогичный генерации схемы по таблице (см. п. 1.5.), только используется язык, наподобие plant uml \ dot (graphviz). Структурные схемы (другие с простой нотацией) и UML строить уже можно, но вот EPC (лучше EPC2, т.е. задание языком специфических правил нотации) и другие со сложной нотацией - нет (красиво не получилось).

Применительно к graphviz: в случае, когда репозитарий объектов хранится в Excel, можно автоматически генерировать схемы, используя инструменты типа: Excel to Graphviz (sourceforge.net).

Пример простого VAD из dot:
digraph g {

rankdir=LR;

node [shape = cds];

Step1 -> Step2 -> Step3 -> Step4;

}

Посмотреть схему можно, вставив код в окно "Online Graphviz Generator":

http://fiane.mooo.com:8080/graphviz/

Кстати, редкий Online Graphviz понимает несокращенный набор параметров спецификации.

Кратко: LR - говорит, что схема строится "слева - направо" (для EPC ставим "сверху - вниз"), cds - это код объекта в виде "кораблика" (VAD). Далее через "->" указывает последовательность процессов. Можно задавать последовательно-параллельные структуры, подписи и тип стрелок, добавлять объекты "исполнители", "продукты" и другие "VAD-примочки", но при этом код становится сложным, а отсутствие нормального управления надписью (перенос, вписывание в фиксированный размер объекта и т.п.) ограничивают применимость инструмента.

Применять подход "скрипт -> схема" можно в сочетании с табличным представлением: например, скриптом VBA читаем поля заполненной пользователем таблички бизнес-процесса (см. 1.5 Из таблицы - схему ) и генерируем dot-последовательность, которую "скармливаем" локальному генератору dot (Graphviz устанавливается на компьютер) или Online Generator. Прямо в word- документе под табличкой "Процесс такой-то" размещаем "кнопочку" и пользователю даем возможность просмотра в графике того, что он ввел в табличку (как он описал в табличной форме свой процесс).

Из "BPM-связанного" особенно удобен dot для построения графов переходов. Если в модели есть docflow с документами со многими состояниями, то без схемы переходов состояний понимание многочисленных переходов осложнено, особенно когда смена состояний документа размазана по многим листам схемы. В итоге заполнив табличку и "скормив" её генератору dot мы увидим всевозможные переходы из состояний. Например, для документа "Отчет" возможны следующие состояния: Шаблон отчета - Шаблон отчета заполнен - Отчет согласован в отделе 1 - Отчет согласован в отделе 2 - Отчет подписан первой подписью - Отчет подписан второй подписью - Отчет оправлен регулятору - Отчет принят регулятором (возможны различные переходы из состояний).

В теме автоматического создания диаграмм из "текстового описания языком" нельзя не упомянуть про Object Process Diagram (OPD) \ Object Process Language (OPL). Тезисы у Object Process Methodology (OPM) вроде как BPM-ориентированные, но поверхностное знакомство с ним породило уверенность, что эта методология намного дальше от "workflow \ business process" (народа), чем те же plant uml \ dot (graphviz). OPCloud доступен тут: https://sandbox.opm.technion.ac.il/

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

2. Другое

2.1 Навигация по связанным моделям (каталог моделей)

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

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

- по дереву моделей (treeview );

- по кликабельным объектам схемы (детализация - проваливаемся в низ, кнопка "выше" - переход к верхнеуровневой модели);

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

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

2.2 Разные фишки и отчеты по атрибутике

Поиск по названиям моделей, атрибутам. Задание правил отбора, например, по диапазону значений последнего редактирования модели. Выгрузка данных фильтрации \ сортировки во внешний файл (отчет), причем разного формата (например, excel для анализа, pdf для презентабельности) и т.п.

Правила работы с одноименными объектами (разрешение конфликтов), например, при наименовании нового объекта система смотрит - использовался ли одноименный объект и при выявлении такового предложит варианты, например, подтвердить или переименовать. У объекта в терминах АРИС только один Definition (Определение объекта, образ), но сколько угодно Occurrence (Отображение объекта, экземпляры на схемах).

2.3 Специфические отчеты

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

Для примера рассмотрим матрицу ответственности\ участия RACI. Требуется автоматическая генерация усеченной RACI-матрицы (здесь показано только для участников процесса, но часто плюс владельцы процесса) по имеющейся, например, VAD-диаграмме (value added chain diagram). Набор ключевых "мега процессов" компании показан в виде VAD и нужно по ним построить (синхронизировать) матрицу участников (RACI по одной только роли "участник процесса").

Рис. 4 Построение RACI матрицы

Алгоритм построения таблицы на VBA Visio\Excel может быть следующий:

1) Создаем в таблице Excel новую строку и в поле "Ключевые процессы" подставляем значение с активного листа visio из объекта типа "название мега процесса".

2) Далее циклом пробегаем по всем VAD-элементам схемы (листа) и через связь (объект "соединитель" для связки с объектами "исполнитель") находим связанные объекты типа "исполнитель" (участник подпроцесса).

3) Находим соответствующее название подразделения в шапке таблицы и на пересечении с процессом ставим символ участия (признак).

4) Переход к следующему листу visio.

Когда в организации десятки подразделений и около сотни "мега процессов" (их выделение достаточно субъективно), то задача синхронизации схем мега-процессов и матрицы участия подразделений в таких ключевых процессах становится достаточно трудоемкой.

2.4 Упаковка необъятной схемы процесса в печатный лист

Когда рисуют гигантскую "портянку" из "тучи элементов" на одной схеме, а потом нужно ее распечатать (А4, А3) или представить в ином интерфейсе (без скролинга такой "портянки"), то возникает ступор. Должна быть поддержка многостраничной схемы и элементов перехода между страницами (в том числе, кликабельными).

2.5 Разное

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

Авто-размещение объектов на схеме: набросал невпопад объекты на лист (главное правильно связи указать и никого не забыть) и нажал кнопку: "расположить как надо" и система сама оптимально и красиво разместила объекты на схеме (в visio функции выравнивания и распределения фигур).

Открытые стандарты хранения и экспорта \ импорта (внешний графический импорт \ экспорт как минимум в visio), как самих графических объектов модели, так и их атрибутов. К сожалению, тот же MS visio так и не научился нормально экспортировать схемы в pdf и svg (например, всплывающие подсказки).

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

Заключение

Устаревшие подходы, реализованные в "современных" платных инструментах моделирования не адекватны времени. За механизацией пришли модные: "информатизация", потом "автоматизация", а теперь и "цифровизация" (аж Digital Transformation / digital disruption и совсем "свежий" Digital process Automation), но возможности инструментов моделирования процессов за три десятка лет почти не изменились. Функциональность древних ARIS, BPwin и т.п. практически не осовременилась в современных BPMS, несмотря на то, что интерес к классическому моделированию процессов постоянно растет, т.к. проблема замены текстовых регламентов на что-то прогрессивное - в целом так и не решена (диаграммы рабочих процессов не заменили текст). Имитационное моделирование и исполняемые модели (также направления, process mining, enterprise architecture - ЕА и др.) - не в счет, рассматриваем "классическое моделирование процессов" - оно же просто "формализация процессов".

Дождаться Open Source системы, в которой было бы реализовано указанное выше, - в обозримом будущем - маловероятно, поэтому, направление улучшайзинга для себя вижу как связку: visio VBA (core, графика) + Excel (как репозитарий для хранения атрибутов моделей, а в будущем и атрибутов графических объектов, инструменты аналитики) + web (publisher & collaboration).

Динозавр - монстр АРИС до сих пор остаётся продуктом 1 в данном сегменте, несмотря на то, что он "заморозился" во времени (в части toolset) и ничего нового в этом направлении не предлагает. АРИС (1994г) и многочисленные visio-надстроенные инструменты (Business Studio, BPM-Х, Orbus iServer и десяток аналогичных) хорошо показали саму концепцию моделирования процессов, которая неизменна десятилетиями. Концептуально подходы понятны, но вот для построения моделей процессов из free взять нечего: через BPMN описать сложные процессы компании - это утопия, если нужно чтобы пользователь понимал нарисованное. Вроде бы удобный трамплин для амбициозного стартапа

Если в CASE, где S="software", еще наблюдается вялотекущая "движуха", например, UML-UML2- SysML или "всяко исполняемое" (no code\ low code), то направление CASE, где S="system" в части BPM (не EA), - фактически "замерло на месте", а робкие попытки, что методологического плана, что инструментального - прежде всего Open Source инструменты "классического" моделирования процессов - скорее отождествляются термином "застой". Правда может я чего-то не заметил.

Немного поутихнет мода на BPMN2 (фетиш в плане замещения нотации ЕРС) и мы вернёмся к "вечному", к классическим подходам BPM, т.к. другого ничего пока так и нет (задачу описания небольших процессов - не рассматриваем). Вернувшись к исходной точке описания процессов, следует смотреть в сторону чего-то интуитивно понятного "простому смертному": бухгалтеру, кассиру, секретарю и т.п., т.е. не программисту. Скорее всего, вернемся к "старине" ЕРС (т.е. фактически к "разбитому корыту") и начнем двигаться к нотации "ЕРС+" (показано на примере ЕРС2) и более гибким (см. предложенные выше фишки) и открытым (free, Open Source) инструментам моделирования. Ориентация на человека, а не машину - ключевой вектор развития. Нотации и инструменты должны быть более "человечными", схемы процессов должны создавать не "специально обученные люди", а сами участники процесса, возможно, даже не подозревая об этом и непосредственно не рисуя процессы.

В 2000-ном году мной использовались ровно такие же подходы и ровно те же инструменты моделирования (основные: ARIS toolset, MS visio), что и сейчас, но тогда была настолько интенсивная "движуха в мире ВРМ", что казалось "вот-вот и прогресс всё поменяет", но это оказалось иллюзией. "Старику ARIS" (в части классического моделирования процессов) на пенсию бы (не смотря на добавленные круглую цифру 10 и магическое слово "cloud"), но похоже перемены придут еще совсем не скоро и светлое будущее обычного BPM откладывается

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

Подробнее..

Интеграция с Госуслугами. Применение Workflow Core (часть II)

23.09.2020 18:06:49 | Автор: admin
В прошлый раз мы рассмотрели место СМЭВ в задаче интеграции с порталом Госуслуг. Предоставляя унифицированный протокол общения между участниками, СМЭВ существенно облегчает взаимодействие между множеством различных ведомств и организаций, желающих предоставлять свои услуги с помощью портала.

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

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

Выбор движка автоматизации бизнес-процессов


Для организации процессной обработки данных существуют библиотеки и системы автоматизации бизнес-процессов, широко представленные на рынке: от встраиваемых решений до полнофункциональных систем, предоставляющих каркас для управления процессами. В качестве средства автоматизации бизнес-процессов мы выбрали Workflow Core. Такой выбор сделан по нескольким причинам: во-первых, движок написан на C# для платформы .NET Core (это наша основная платформа для разработки), поэтому включить его в общую канву продукта проще, в отличие от, например, Camunda BPM. Кроме того, это встраиваемый (embedded) движок, что даёт широкие возможности по управлению экземплярами бизнес-процессов. Во-вторых, среди множества поддерживаемых вариантов хранения данных есть и используемый в наших решениях PostgreSQL. В-третьих, движок предоставляет простой синтаксис для описания процесса в виде fluent API (также есть вариант описания процесса в JSON-файле, однако, он показался менее удобным для использования в силу того, что становится сложно обнаружить ошибку в описании процесса до момента его фактического выполнения).

Бизнес-процессы


Среди общепринятых инструментов описания бизнес-процессов следует отметить нотацию BPMN. Например, решение задачи FizzBuzz в нотации BPMN может выглядеть так:


Движок Workflow Core содержит большинство стандартных блоков и операторов, представленных в нотации, и, как уже говорилось выше, позволяет пользоваться fluent API или данными в формате JSON для описания конкретных процессов. Реализация этого процесса средствами движка Workflow Core может принять такой вид:

// Класс с данными процесса.public class FizzBuzzWfData{  public int Counter { get; set; } = 1;  public StringBuilder Output { get; set; } = new StringBuilder();}// Описание процесса.public class FizzBuzzWorkflow : IWorkflow<FizzBuzzWfData>{  public string Id => "FizzBuzz";  public int Version => 1;  public void Build(IWorkflowBuilder<FizzBuzzWfData> builder)  {    builder      .StartWith(context => ExecutionResult.Next())      .While(data => data.Counter <= 100)        .Do(a => a          .StartWith(context => ExecutionResult.Next())            .Output((step, data) => data.Output.Append(data.Counter))          .If(data => data.Counter % 3 == 0 || data.Counter % 5 == 0)            .Do(b => b              .StartWith(context => ExecutionResult.Next())                .Output((step, data) => data.Output.Clear())              .If(data => data.Counter % 3 == 0)                .Do(c => c                  .StartWith(context => ExecutionResult.Next())                    .Output((step, data) =>                       data.Output.Append("Fizz")))              .If(data => data.Counter % 5 == 0)                .Do(c => c                  .StartWith(context => ExecutionResult.Next())                    .Output((step, data) =>                      data.Output.Append("Buzz"))))              .Then(context => ExecutionResult.Next())                .Output((step, data) =>                {                  Console.WriteLine(data.Output.ToString());                  data.Output.Clear();                  data.Counter++;                }));  }}

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

Метод Build получает в качестве аргумента объект построителя процесса, который служит отправной точкой для вызова цепочки методов расширения, последовательно описывающих шаги бизнес-процесса. Методы расширения, в свою очередь, могут содержать описание действий непосредственно в текущем коде в виде лямбда-выражений, переданных в качестве аргументов, а могут быть параметризованными. В первом случае, который представлен в листинге, простой алгоритм выливается в достаточно непростой набор инструкций. Во втором логика шагов прячется в отдельных классах-наследниках от типа Step (или AsyncStep для асинхронных вариантов), что позволяет вместить сложные процессы в более лаконичное описание. На практике более пригодным представляется второй подход, первый же достаточен для простых примеров или предельно простых бизнес-процессов.

Собственно класс-описание процесса реализует параметризованный интерфейс IWorkflow, и, выполняя контракт, содержит идентификатор и номер версии процесса. Благодаря этим сведениям движок способен порождать экземпляры процессов в памяти, наполняя их данными и фиксируя их состояние в хранилище. Поддержка версионности позволяет создавать новые вариации процессов без риска затронуть существующие в хранилище экземпляры. Для создания новой версии достаточно создать копию имеющегося описания, присвоить очередной номер свойству Version и изменить нужным образом поведение этого процесса (идентификатор при этом следует оставить неизменным).

Примерами бизнес-процессов в контексте нашей задачи служат:

  • Опрос очереди сообщений СМЭВ с новыми заявлениями на получение услуг обеспечивает периодический мониторинг очереди сообщений СМЭВ на предмет наличия новых заявлений от пользователей портала.
  • Обработка заявления содержит шаги по анализу содержимого заявления, отражению его в ИАС, отправке подготовленных результатов на портал.
  • Обработка запроса на отмену заявления позволяет отразить факт отмены поданного с портала заявления на услугу в ИАС.
  • Опрос очереди сообщений СМЭВ с ответами на запросы о смене статуса заявления делает возможным изменять статус заявления на портале через ИАС, периодически контролируя очередь ответов портала на поданные запросы о смене статуса.

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

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

public class LoadRequestWf : IWorkflow<LoadRequestWfData>{  public const string DefinitionId = "LoadRequest";  public string Id => DefinitionId;  public int Version => 1;  public void Build(IWorkflowBuilder<LoadRequestWfData> builder)  {    builder      .StartWith(then => ExecutionResult.Next())        .While(d => !d.Quit)          .Do(x => x            .StartWith<LoadRequestStep>() // *              .Output(d => d.LoadRequest_Output, s => s.Output)            .If(d => d.LoadRequest_Output.Exception != null)              .Do(then => then                .StartWith(ctx => ExecutionResult.Next()) // *                  .Output((s, d) => d.Quit = true))            .If(d => d.LoadRequest_Output.Exception == null                && d.LoadRequest_Output.Result.SmevReqType                  == ReqType.Unknown)              .Do(then => then                .StartWith<LogInfoAboutFaultResponseStep>() // *                  .Input((s, d) =>                    { s.Input = d.LoadRequest_Output?.Result?.Fault; })                  .Output((s, d) => d.Quit = false))            .If(d => d.LoadRequest_Output.Exception == null               && d.LoadRequest_Output.Result.SmevReqType                 == ReqType.DataRequest)              .Do(then => then                .StartWith<StartWorkflowStep>() // *                  .Input(s => s.Input, d => BuildEpguNewApplicationWfData(d))                  .Output((s, d) => d.Quit = false))            .If(d => d.LoadRequest_Output.Exception == null              && d.LoadRequest_Output.Result.SmevReqType == ReqType.Empty)              .Do(then => then                .StartWith(ctx => ExecutionResult.Next()) // *                  .Output((s, d) => d.Quit = true))          .If(d => d.LoadRequest_Output.Exception == null             && d.LoadRequest_Output.Result.SmevReqType               == ReqType.CancellationRequest)            .Do(then => then              .StartWith<StartWorkflowStep>() // *                .Input(s => s.Input, d => BuildCancelRequestWfData(d))                .Output((s, d) => d.Quit = false)));  }}

В строках, отмеченных *, можно наблюдать использование параметризованных методов расширения, которые инструктируют движок о необходимости использовать классы шагов (об этом далее), соответствующие параметрам-типам. С помощью методов расширения Input и Output мы имеем возможность задавать исходные данные, передаваемые шагу перед началом выполнения, и, соответственно, изменить данные процесса (а они представлены экземпляром класса LoadRequestWfData) в связи с произведёнными шагом действиями. А так процесс выглядит на BPMN-диаграмме:


Шаги


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

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

Отправка подтверждающих (Ack) запросов о получении ответа.

  • Выгрузка файлов в файловое хранилище.
  • Извлечение данных из пакета СМЭВ и т.п.

Специфические шаги:

  • Создание объектов в ИАС, обеспечивающих возможность оператору предоставить услугу.
  • Генерирование документов заданной структуры с данными из заявления и размещение их в ИАС.
  • Отправка результата оказания услуги на портал и т.п.

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

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

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

Сервисы


Сервисы представляют собой следующий, более низкий уровень решения задач. Каждый шаг при выполнении своей обязанности опирается, как правило, на один или более сервисов (N.B. Понятие сервис в данном контексте более близко к аналогичному понятию сервис уровня приложения из области предметно-ориентированного проектирования (DDD)).

Примерами сервисов служат:

  • Сервис получения ответа из очереди ответов СМЭВ готовит соответствующий пакет данных в формате SOAP, отправляет его в СМЭВ и преобразует ответ в вид, пригодный для дальнейшей обработки.
  • Сервис загрузки файлов из хранилища СМЭВ обеспечивает считывание файлов, приложенных к заявлению с портала, из файлового хранилища по протоколу FTP.
  • Сервис получения результата оказания услуги считывает из ИАС данные о результатах услуги и формирует соответствующий объект, на основе которого другой сервис построит SOAP-запрос для отправки на портал.
  • Сервис выгрузки файлов, связанных с результатом оказания услуги, в файловое хранилище СМЭВ.

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

  • Сервисы СМЭВ.
  • Сервисы ИАС.

Сервисы для работы со внутренней инфраструктурой интеграционного решения (журналирование информации о пакетах данных, связывание сущностей интеграционного решения с объектами ИАС и пр.).

В архитектурном плане сервисы являются наиболее низким уровнем, однако, они при решении своих задач также могут опираться на утилитарные классы. Так, например, в решении существует пласт кода, решающий задачи сериализации и десериализации SOAP-пакетов данных для разных версий протокола СМЭВ. В общем виде приведённое выше описание можно резюмировать в диаграмме классов:


Непосредственно к движку относятся интерфейс IWorkflow и абстрактный класс StepBodyAsync (впрочем, можно использовать и его синхронный аналог StepBody). Ниже на схеме представлены реализации строительных блоков конкретные классы с описаниями бизнес-процессов Workflow и используемые в них шаги (Step). На нижнем уровне представлены сервисы, которые, по существу, являются уже спецификой именно данной реализации решения и, в отличие от процессов и шагов не являются обязательными.

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

Встраивание движка в решение


На момент начала создания системы интеграции с порталом в репозитории Nuget была доступна версия движка 2.1.2. Он встраивается в контейнер зависимостей стандартным образом в методе ConfigureServices класса Startup:

public void ConfigureServices(IServiceCollection services){  // ...  services.AddWorkflow(opts =>    opts.UsePostgreSQL(connectionString, false, false, schemaName));  // ...}

Движок можно настроить на одно из поддерживаемых хранилищ данных (среди таковых есть и другие: MySQL, MS SQL, SQLite, MongoDB). В случае PostgreSQL для работы с процессами движок использует Entity Framework Core в варианте Code First. Соответственно, при наличии пустой базы данных есть возможность применить миграцию и получить нужную структуру таблиц. Применение миграции является опциональным, этим можно управлять с помощью аргументов метода UsePostgreSQL: второй (canCreateDB) и третий (canMigrateDB) аргументы логического типа позволяют сообщить движку, может ли он создать БД при её отсутствии и применять миграции.

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

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

Каждый модуль содержит описания процессов, инфраструктуру для выполнения бизнес-логики, а также некоторый унифицированный код для предоставления возможности разработанной системе интеграции распознать и динамически загрузить описания процессов в экземпляр движка Workflow Core:



Регистрация и запуск бизнес-процессов


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

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

public async Task RunWorkflowsAsync(IWorkflowHost host,  CancellationToken token){  host.RegisterWorkflow<LoadRequestWf, LoadRequestWfData>();  // Регистрируем другие процессы...  await host.StartAsync(token);  token.WaitHandle.WaitOne();  host.Stop();}

Заключение


В общих чертах мы рассмотрели действия, которые необходимо предпринять для использования Workflow Core в интеграционном решении. Движок позволяет описывать бизнес-процессы в достаточно гибкой и удобной манере. Держа в уме тот факт, что мы имеем дело с задачей интеграции с порталом Госуслуг посредством СМЭВ, следует ожидать, что проектируемые бизнес-процессы будут охватывать спектр довольно разнообразных задач (опрос очереди, загрузка/выгрузка файлов, гарантирование соблюдения протокола обмена и обеспечение подтверждения получения данных, обработка ошибок на разных этапах и т.п.). Отсюда вполне естественным будет ожидать возникновения некоторых на первый взгляд неочевидных моментов реализации, и именно им мы посвятим следующую, заключительную статью цикла.

Ссылки для изучения


Подробнее..
Категории: C , Net , Бизнес-процессы , Workflow , Netcore

Интеграция с Госуслугами. Особенности реализации задачи средствами Workflow Core (часть III)

29.12.2020 16:19:06 | Автор: admin
Ранее мы рассмотрели роль СМЭВ в обеспечении работоспособности портала Госуслуг, а также общие принципы организации взаимодействия с ним на стороне поставщика сведений посредством Workflow Core.

Поскольку задача интеграции решается через посредника (СМЭВ) и, помимо прочего, с использованием движка, опыта работы с которым прежде не было, то наивно будет ожидать, что всё пройдёт гладко. Некоторым сложностям, с которыми мы встретились при решении задачи, посвящена данная статья.


Реализация циклических бизнес-процессов


Внешние циклы


Как было отмечено ранее, некоторые процессы предполагают периодическое повторение включённых в них действий. Рассмотрим процесс опроса очереди СМЭВ (пример которого был приведён в предыдущей статье) на наличие новых заявлений на оказание услуг: заявления, поданные пользователями портала, помещаются в очередь запросов СМЭВ. Мы, как сторона, отвечающая на запросы (поставщики в терминологии СМЭВ), должны периодически опрашивать эту очередь с помощью специальных SOAP-запросов. Если очередь не пуста, то забираем имеющиеся в ней запросы. Когда все запросы выбраны из очереди, СМЭВ даёт пустой ответ, означающий, что данных для нас больше нет.

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

В переложении на схему BPMN процесс может выглядеть так:



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

С точки зрения бизнес-логики всё организовано верно и процесс выполняет свою задачу, но на практике с течением времени всё складывается не так безупречно. Дело в том, что при работе в режиме фиксации состояния процессов в хранилище данных (persistence mode) факты выполнения каждого шага находят своё отображение в соответствующих таблицах БД. Чем дольше работает процесс, тем большую часть эфирного времени по его обработке начинают занимать операции обмена данными между движком и базой. Это приводит к тому, что производительность системы, выражающаяся, в частности, в скорости движения процессов по шагам и, как следствие, в обработке поступающих заявлений, падает практически до нуля.

Так, описанный выше процесс мы проверяли на интервале 5 сек. (такое сокращение с изначальных 10 минут было допущено на этапе отладки и на первых порах опытной эксплуатации). Суточный прогон такого процесса оставлял за собой след из порядка 17200 записей в соответствующей таблице ExecutionPointer. Пары суток работы без ручного вмешательства хватало для того, чтобы процесс переставал функционировать, что закономерно приводило к невозможности получения новых заявлений.

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

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

Реализация циклов внутри процессов


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

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



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

Решением в этом случае является использование механизма событий. Отправной точкой для событий служит экземпляр движка бизнес-процессов, который можно получить в любом месте, где доступно использование инъекций зависимостей или под рукой имеется экземпляр поставщика служб IServiceProvider, запросив объект IWorkflowHost. Например, в нашем решении одним из таких мест служит метод, вызываемый нажатием кнопки отправки результата в ИАС. А вот и инструкция, инициирующая запуск события:

await host.PublishEvent(Workflow.Events.SEND_FINAL_RESULT_EVENT,  workflow.Id, null);

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

Подписка на событие происходит с помощью метода расширения WaitFor, вызванного в теле описания бизнес-процесса:

// ....WaitFor(Workflow.Events.SEND_FINAL_RESULT_EVENT,  (data, step) => s.Workflow.Id)// ...

Состав аргументов прежний: первый отвечает за название прослушиваемого события, второй позволяет передать лямбда-выражение, вычисляющее ключ события. Данные, которые могут быть переданы вместе с событием, обрабатываются последующим вызовом метода расширения Output. Вот пример ожидания и обработки другого события:

// ....WaitFor(Workflow.Events.INTERMEDIATE_STATUS_RESPONSE,    (data, step) => s.Workflow.Id)  .Output((e, d) =>  {    if (e.EventData == null)      throw new Exception("В событии нет данных.");    if (e.EventData is IntermediateStatusEventResponse eventResult)      d.IntermediateStatusWaitEvent_Output = eventResult;    else      throw new Exception("Неожиданный тип данных в событии.");})// ...

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

Обработка ошибок и журналирование


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

Ошибки можно подразделить на три категории:
  • Ошибки ввиду сбоев связи (напомним, что взаимодействие через СМЭВ ведётся посредством защищённых каналов, которые могут по какой-то причине выйти из строя, а то и вовсе не быть в него введёнными).
  • Ошибки уровня протокола обмена данными (например, мы послали некорректный запрос, на который получили ответ с ошибкой в соответствующем специальном блоке пакета данных).
  • Непредвиденные или специально запущенные исключения в коде реализации логики шагов.

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

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

// ....Then<ExtractSmevPackageDataRequestInfoStep>()  .Input((step, data) =>  {    step.Input = new ExtractSmevPackageDataRequestInfoStep_Input    {      RegisteredApplicationKey = data.RegisteredApplicationKey,      SmevPackageXml = data.Input.SmevPackageXmlBody    };  })  .Output((step, data) =>    data.ExtractSmevPackageDataRequestInfoStep_Output = step.Output)  .OnError(WorkflowErrorHandling.Terminate)// ...

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

Кроме описанного выше способа (Terminate) движок располагает ещё тремя:
  • Compensate выполнение компенсационного шага, т.е. некоторого известного действия на случай ошибки, призванного, например, откатить возможные изменения (этот термин относится к области т.н. Saga-транзакций, которые, к слову, тоже поддерживаются движком).
  • Suspend приостановка процесса с возможностью последующего продолжения выполнения по команде извне.
  • Retry уже знакомый нам вариант по умолчанию, предполагающий перезапуск шага через одну минуту (интервал можно регулировать с помощью второго аргумента метода OnError).

Кроме описанного выше порядка работы с ошибками, стоит отметить ещё один способ сообщение о проблеме с помощью выходных данных шага, например, в виде некоторого логического значения. Далее с помощью ветвления описывается способ реагирования на эту ситуацию, приемлемый в данном контексте, будь то завершение процесса или отправка сообщения в СМЭВ.

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

Работа с Saga-транзакциями


Отдельного внимания заслуживает использование транзакционности в Workflow Core. Данное средство помогает организовать последовательность шагов процесса таким образом, что любая ошибка, возникшая в одном из них, позволяет организовать реакцию по откату результатов каждого шага или транзакции в целом. Ниже приведён пример участка бизнес-процесса, описывающего последовательность шагов по отправке результата оказания услуги на портал:

// ....WaitFor(Workflow.Events.SEND_FINAL_RESULT_EVENT,  (d, s) => s.Workflow.Id, d => DateTime.Now.ToUniversalTime()).Then(o => ExecutionResult.Next()).Saga(saga => saga // *  .StartWith<CheckFinalResultQueueStep>()    .Input((s, d) => { /* ... */ })    .Output((s, d) => { /* ... */ })  .If(d => d.CheckFinalResultQueueStep_Output.Data != null      && d.CheckFinalResultQueueStep_Output.Data.IsSent)    .Do(f => f      .StartWith(r => ExecutionResult.Next())      .EndWorkflow())  .If(d => d.CheckFinalResultQueueStep_Output.Data != null      && !d.CheckFinalResultQueueStep_Output.Data.IsSent)    .Do(f => f      .StartWith(r => ExecutionResult.Next())      .Parallel()        .Do(resultSendingBranch => resultSendingBranch          .StartWith<UploadFilesToSmevStep>()            .Input((s, d) => { /* ... */ })            .Output((s, d) => { /* ... */ })          .Then<SendFinalApplicationStatusStep>()            .Input((s, d) => { /* ... */ })            .Output((s, d) => { /* ... */ })          .Then<Steps.SaveSentFinalStatusInformationToIasStep>()            .Input((s, d) => { /* ... */ }))        .Do(eventEmitBranch => eventEmitBranch          .StartWith<PublishEventStep>()            .Input((s, d) => { /* ... */ })))      .Join()      .EndWorkflow()).OnError(WorkflowErrorHandling.Retry, // **  TimeSpan.FromSeconds(DEFAULT_ONERROR_RETRY_INTERVAL))// ...

Здесь важно отметить, что при описании транзакций крайне желательно явным образом задавать способ реагирования на ошибки (строка ** для соответствующей транзакции, открытой на строке *). Дело в том, что отсутствие такого указания приведёт к прекращению выполнения ветки процесса, обёрнутой в транзакцию. Это может стать большой неожиданностью, особенно на этапе опытной эксплуатации. Конкретно для приведённого выше примера отсутствие вызова метода расширения OnError означало бы, что, скажем, ошибка в шаге CheckFinalResultQueueStep (на котором делается обращение к таблице в БД) приведёт к тому, что результат, подготовленный оператором ИАС, никогда уйдёт на портал. И наоборот, наличие явно указанной реакции на ошибку позволит повторить всю последовательность шагов через указанный интервал времени и с большой вероятностью гарантировать, что рано или поздно результат будет доставлен адресату.

Отказы при растущих объёмах данных


С течением времени количество данных (в том числе служебных) о бизнес-процессах в хранилище неизбежно растёт. Опыт использования Workflow Core показал, что этот рост приводит к постепенному замедлению работы движка с последующим отказом в работе. По достижении критической точки процессы начинают зависать на некоторых шагах без явных предпосылок к этому из текущих данных или логики описания. Более конкретно: шаги таких процессов останавливаются в статусе 1 (Pending) и не находят дальнейшего продвижения по цепочке состояний.

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

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

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

Новые версии бизнес-процессов


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

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

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

Особенности отладки и тестирования


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

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

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

Кроме того, заметим, что для тестирования непосредственно бизнес-процессов существует специальный Nuget-пакет. А тестирование отдельных шагов или сервисов вполне укладывается в парадигму модульного тестирования.

Общие рекомендации


Подводя итог сказанному выше, можно выделить следующие общие рекомендации по использованию движка в задачах, подобных описанной:
  • Постарайтесь важные для бизнес-процесса действия вынести на уровень его описания, не пряча логику в шаги или более низкие уровни. Это повысит прозрачность и читаемость процесса, позволит легче диагностировать проблемы.
  • Аккуратно используйте циклы и по возможности заменяйте их механизмом событий в задачах, требующих ожидания некоторых внешних действий.
  • Обеспечьте достаточный уровень журналирования, это также облегчит диагностику ошибок.
  • Предусмотрите способ реагирования на растущее со временем количество данных по бизнес-процессам, в том числе завершённым. Некоторые данные перестают представлять какую-либо ценность и от них можно избавиться.
  • Соблюдайте осторожность при вводе в эксплуатацию новых версий бизнес-процессов. Постарайтесь предварительно провести разностороннюю проверку их работоспособности. Любое, даже самое незначительное изменение в описании процесса (на уровне порядка и состава шагов), следует оформлять отдельной его версией.
  • Заранее подумайте о средствах, которые облегчат отладку и тестирование разрабатываемого решения. Здесь подойдут как модульные тесты, так и самодельные заглушки, имитирующие в необходимых пределах работу СМЭВ.

Заключение


Итак, наше интеграционное решение было запущено в работу в январе 2020 г. Первое заявление от пользователя портала получено 24 января. С тех пор через СМЭВ с портала Госуслуг операторы системы получили и обработали порядка 8000 заявлений от граждан и юридических лиц. На диаграмме ниже представлена динамика поступления новых заявлений в период с января по декабрь:



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

Благодаря возможностям версионирования процессов и событий, поддерживаемых движком, открыта возможность для расширения спектра решаемых задач. Так, к существующей функциональности мы добавили интеграцию с многофункциональными центрами (МФЦ), вследствие чего пользователям портала стало доступно получение результатов услуг не только в электронном виде, почтовым отправлением или лично в ведомстве, но и посредством МФЦ. В рамках этой работы существующие бизнес-процессы претерпели минимальные изменения (с порождением соответствующих новых версий), при этом появились и новые, с которыми был организован обмен данными через события.

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

Ссылки для изучения


Подробнее..
Категории: C , Net , Бизнес-процессы , Workflow , Netcore

Ваш безлимит как увеличить пропускную способность автомерджа

21.06.2021 14:12:41 | Автор: admin

Отыщи всему начало, и ты многое поймёшь (Козьма Прутков).

Меня зовут Руслан, я релиз-инженер в Badoo и Bumble. Недавно я столкнулся с необходимостью оптимизировать механизм автомерджа в мобильных проектах. Задача оказалась интересной, поэтому я решил поделиться её решением с вами. В статье я расскажу, как у нас раньше было реализовано автоматическое слияние веток Git и как потом мы увеличили пропускную способность автомерджа и сохранили надёжность процессов на прежнем высоком уровне.

Свой автомердж

Многие программисты ежедневно запускают git merge, разрешают конфликты и проверяют свои действия тестами. Кто-то автоматизирует сборки, чтобы они запускались автоматически на отдельном сервере. Но решать, какие ветки сливать, всё равно приходится человеку. Кто-то идёт дальше и добавляет автоматическое слияние изменений, получая систему непрерывной интеграции (Continuous Integration, или CI).

Например, GitHub предлагает полуручной режим, при котором пользователь с правом делать записи в репозиторий может поставить флажок Allow auto-merge (Разрешить автомердж). При соблюдении условий, заданных в настройках, ветка будет соединена с целевой веткой. Bitbucket поддерживает большую степень автоматизации, накладывая при этом существенные ограничения на модель ветвления, имена веток и на количество мерджей.

Такой автоматизации может быть достаточно для небольших проектов. Но с увеличением количества разработчиков и веток, ограничения, накладываемые сервисами, могут существенно повлиять на производительность CI. Например, раньше у нас была система мерджа, при которой основная ветка всегда находилась в стабильном состоянии благодаря последовательной стратегии слияний. Обязательным условием слияния была успешная сборка при наличии всех коммитов основной ветки в ветке разработчика. Работает эта стратегия надёжно, но у неё есть предел, определяемый временем сборки. И этого предела оказалось недостаточно. При времени сборки в 30 минут на обработку 100 слияний в день потребовалось бы более двух суток. Чтобы исключить ограничения подобного рода и получить максимальную свободу выбора стратегий мерджа и моделей ветвления, мы создали собственный автомердж.

Итак, у нас есть свой автомердж, который мы адаптируем под нужды каждой команды. Давайте рассмотрим реализацию одной из наиболее интересных схем, которую используют наши команды Android и iOS.

Термины

Main. Так я буду ссылаться на основную ветку репозитория Git. И коротко, и безопасно. =)

Сборка. Под этим будем иметь в виду сборку в TeamCity, ассоциированную с веткой Git и тикетом в трекере Jira. В ней выполняются как минимум статический анализ, компиляция и тестирование. Удачная сборка на последней ревизии ветки в сочетании со статусом тикета To Merge это однo из необходимых условий автомерджа.

Пример модели ветвления

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

На основе ветки main разработчик создаёт ветку с названием, включающим идентификатор тикета в трекере, например PRJ-k. По завершении работы над тикетом разработчик переводит его в статус Resolved. При помощи хуков, встроенных в трекер, мы запускаем для ветки тикета сборку. В определённый момент, когда изменения прошли ревью и необходимые проверки автотестами на разных уровнях, тикет получает статус To Merge, его забирает автоматика и отправляет в main.

Раз в неделю на основе main мы создаём ветку релиза release_x.y.z, запускаем на ней финальные сборки, при необходимости исправляем ошибки и наконец выкладываем результат сборки релиза в App Store или Google Play. Все фазы веток отражаются в статусах и дополнительных полях тикетов Jira. В общении с Jira помогает наш клиент REST API.

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

Первая версия: жадная стратегия

Сначала мы шли от простого и очевидного. Брали все тикеты, находящиеся в статусе To Merge, выбирали из них те, для которых есть успешные сборки, и отправляли их в main командой git merge, по одной.

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

Наличие в TeamCity актуальной успешной сборки мы проверяли при помощи метода REST API getAllBuilds примерно следующим образом (псевдокод):

haveFailed = False # Есть ли неудачные сборкиhaveActive = False # Есть ли активные сборки# Получаем сборки типа buildType для коммита commit ветки branchbuilds = teamCity.getAllBuilds(buildType, branch, commit)# Проверяем каждую сборкуfor build in builds:  # Проверяем каждую ревизию в сборке  for revision in build.revisions:    if revision.branch is branch and revision.commit is commit:      # Сборка актуальна      if build.isSuccessful:        # Сборка актуальна и успешна        return True      else if build.isRunning or build.isQueued        haveActive = True      else if build.isFailed:        haveFailed = Trueif haveFailed:  # Исключаем тикет из очереди, переоткрывая его  ticket = Jira.getTicket(branch.ticketKey)  ticket.reopen("Build Failed")  return Falseif not haveActiveBuilds:  # Нет ни активных, ни упавших, ни удачных сборок. Запускаем новую  TriggerBuild(buildType, branch)

Ревизии это коммиты, на основе которых TeamCity выполняет сборку. Они отображаются в виде 16-ричных последовательностей на вкладке Changes (Изменения) страницы сборки в веб-интерфейсе TeamCity. Благодаря ревизиям мы можем легко определить, требуется ли пересборка ветки тикета или тикет готов к слиянию.

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

Так как после перевода тикета в статус готовности (в нашем примере Resolved) соответствующая ветка, как правило, не меняется, то и сборка, ассоциированная с тикетом, чаще всего остаётся актуальной. Кроме того, сам факт нахождения тикета в статусе To Merge говорит о высокой вероятности того, что сборка не упала. Ведь при падении сборки мы сразу переоткрываем тикет.

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

Всё работало быстро, но требовало внимания. То и дело возникали ситуации, когда изменения нескольких тикетов конфликтовали между собой. Конфликты при слияниях как явление достаточно распространённое поначалу ни у кого особых вопросов не вызывали. Их разрешали разработчики, дежурные по релизу. Но с увеличением количества разработчиков, задач и, соответственно, веток, приведение релиза в порядок требовало всё больше усилий. Задержки в разрешении конфликтов начали сказываться на новых задачах. Полагаю, не стоит продолжать эту цепочку скорее всего, вы уже поняли, что я имею в виду. С конфликтами нужно было что-то делать, причём не допуская их попадания в релиз.

Конфликты слияния

Если изменить одну и ту же строку кода в разных ветках и попытаться соединить их в main, то Git попросит разрешить конфликты слияния. Из двух вариантов нужно выбрать один и закоммитить изменения.

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

Если команда git merge завершилась с ошибкой и для всех файлов в списке git ls-files --unmerged заданы обработчики конфликтов, то для каждого такого файла мы выполняем парсинг содержимого по маркерам конфликтов <<<<<<<, ======= и >>>>>>>. Если конфликты вызваны только изменением версии приложения, то, например, выбираем последнюю версию между локальной и удалённой частями конфликта.

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

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

Логические конфликты

А может ли случиться так, что, несмотря на успешность сборок пары веток в отдельности, после слияния их с main сборка на основной ветке упадёт? Практика показывает, что может. Например, если сумма a и b в каждой из двух веток не превышает 5, то это не гарантирует того, что совокупные изменения a и b в этих ветках не приведут к большей сумме.

Попробуем воспроизвести это на примере Bash-скрипта test.sh:

#!/bin/bashget_a() {    printf '%d\n' 1}get_b() {    printf '%d\n' 2}check_limit() {    local -i value="$1"    local -i limit="$2"    if (( value > limit )); then        printf >&2 '%d > %d%s\n' "$value" "$limit"        exit 1    fi}limit=5a=$(get_a)b=$(get_b)sum=$(( a + b ))check_limit "$a" "$limit"check_limit "$b" "$limit"check_limit "$sum" "$limit"printf 'OK\n'

Закоммитим его и создадим пару веток: a и b.
Пусть в первой ветке функция get_a() вернёт 3, а во второй get_b() вернёт 4:

diff --git a/test.sh b/test.shindex f118d07..39d3b53 100644--- a/test.sh+++ b/test.sh@@ -1,7 +1,7 @@ #!/bin/bash get_a() {-    printf '%d\n' 1+    printf '%d\n' 3 } get_b() {git diff main bdiff --git a/test.sh b/test.shindex f118d07..0bd80bb 100644--- a/test.sh+++ b/test.sh@@ -5,7 +5,7 @@ get_a() { }  get_b() {-    printf '%d\n' 2+    printf '%d\n' 4 }  check_limit() {

В обоих случаях сумма не превышает 5 и наш тест проходит успешно:

git checkout a && bash test.shSwitched to branch 'a'OKgit checkout b && bash test.shSwitched to branch 'b'OK

Но после слияния main с ветками тесты перестают проходить, несмотря на отсутствие явных конфликтов:

git merge a bFast-forwarding to: aTrying simple merge with bSimple merge did not work, trying automatic merge.Auto-merging test.shMerge made by the 'octopus' strategy. test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)bash test.sh7 > 5

Было бы проще, если бы вместо get_a() и get_b() использовались присваивания: a=1; b=2, заметит внимательный читатель и будет прав. Да, так было бы проще. Но, вероятно, именно поэтому встроенный алгоритм автомерджа Git успешно обнаружил бы конфликтную ситуацию (что не позволило бы продемонстрировать проблему логического конфликта):

git merge a Updating 4d4f90e..8b55df0Fast-forward test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)git merge b Auto-merging test.shCONFLICT (content): Merge conflict in test.shRecorded preimage for 'test.sh'Automatic merge failed; fix conflicts and then commit the result.

Разумеется, на практике конфликты бывают менее явными. Например, разные ветки могут полагаться на API разных версий какой-нибудь библиотеки зависимости, притом что более новая версия не поддерживает обратной совместимости. Без глубоких знаний кодовой базы (читай: без разработчиков проекта) обойтись вряд ли получится. Но ведь CI как раз и нужен для решения таких проблем.

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

Превентивные меры

Итак, главное не допустить попадания логического конфликта в main. Иначе придётся долго и мучительно искать источник ошибок, а затем программиста, который проблему должен или может решить. Причём делать это нужно максимально быстро и качественно, чтобы, во-первых, не допустить задержки релиза и во-вторых, избежать в новых ветках логических конфликтов, основанных на уже выявленном конфликте. Такие конфликты часто приводят к неработоспособности большой части приложения или вовсе блокируют его запуск.

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

Вторая версия: последовательная стратегия

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

Git, по идее, как раз и является средством синхронизации. Но порядок попадания веток в main и, наоборот, main в ветки определяем мы сами. Чтобы определить точно, какие из веток вызывают проблемы в main, можно попробовать отправлять их туда по одной. Тогда можно выстроить их в очередь, а порядок организовать на основе времени попадания тикета в статус To Merge в стиле первый пришёл первым обслужен.

С порядком определились. А как дальше соединять ветки? Допустим, мы сольём в main первый тикет из очереди. Так как main изменилась, она может конфликтовать с остальными тикетами в очереди. Поэтому перед тем как сливать следующий тикет, нужно удостовериться, что обновлённая main по-прежнему совместима с ним. Для этого достаточно слить main в тикет. Но так как после соединения main с веткой её состояние отличается от того, которое было в сборке, необходимо перезапустить сборку. Для сохранения порядка все остальные тикеты в очереди должны ждать завершения сборки и обработки впередистоящих тикетов. Примерно такие рассуждения привели нас к последовательной стратегии автомерджа.

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

Но есть у этой схемы существенный недостаток: пропускная способность автомерджа линейно зависит от времени сборки. При среднем времени сборки iOS-приложения в 25 минут мы можем рассчитывать на прохождение максимум 57 тикетов в сутки. В случае же с Android-приложением требуется примерно 45 минут, что ограничивает автомердж 32 тикетами в сутки, а это даже меньше количества Android-разработчиков в нашей компании.

На практике время ожидания тикета в статусе To Merge составляло в среднем 2 часа 40 минут со всплесками, доходящими до 10 часов! Необходимость оптимизации стала очевидной. Нужно было увеличить скорость слияний, сохранив при этом стабильность последовательной стратегии.

Финальная версия: сочетание последовательной и жадной стратегий

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

Давайте вспомним идею жадной стратегии: мы сливали все ветки готовых тикетов в main. Основной проблемой было отсутствие синхронизации между ветками. Решив её, мы получим быстрый и надёжный автомердж!

Раз нужно оценить общий вклад всех тикетов в статусе To Merge в main, то почему бы не слить все ветки в некоторую промежуточную ветку Main Candidate (MC) и не запустить сборку на ней? Если сборка окажется успешной, то можно смело сливать MC в main. В противном случае придётся исключать часть тикетов из MC и запускать сборку заново.

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

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

Следуя этому алгоритму, для k проблемных тикетов в худшем случае нам придётся выполнить O(k*log2(n)) сборок, прежде чем мы обработаем все проблемные тикеты и получим удачную сборку на оставшихся.

Вероятность благоприятного исхода велика. А ещё в то время, пока сборки на ветке MC падают, мы можем продолжать работу при помощи последовательного алгоритма!

Итак, у нас есть две автономные модели автомерджа: последовательная (назовём её Sequential Merge, или SM) и жадная (назовём её Greedy Merge, или GM). Чтобы получить пользу от обеих, нужно дать им возможность работать параллельно. А параллельные процессы требуют синхронизации, которой можно добиться либо средствами межпроцессного взаимодействия, либо неблокирующей синхронизацией, либо сочетанием этих двух методов. Во всяком случае, мне другие методы неизвестны.

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

Остаётся предотвратить все возможные случаи состояний гонки. Их много, но для понимания сути приведу несколько самых важных:

  1. SM-SM и GM-GM: между командами одного типа.

  2. SM-GM: между SM и GM в рамках одного репозитория.

Первая проблема легко решается при помощи мьютекса по токену, включающему в себя имя команды и название репозитория. Пример: lock_${command}_${repository}.

Поясню, в чём заключается сложность второго случая. Если SM и GM будут действовать несогласованно, то может случиться так, что SM соединит main с первым тикетом из очереди, а GM этого тикета не заметит, то есть соберёт все остальные тикеты без учёта первого. Например, если SM переведёт тикет в статус In Master, а GM будет всегда выбирать тикеты по статусу To Merge, то GM может никогда не обработать тикета, соединённого SM. При этом тот самый первый тикет может конфликтовать как минимум с одним из других.

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

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

Немного о TeamCity

В процессе реализации GM нам предстояло обработать много нюансов, которыми я не хочу перегружать статью. Но один из них заслуживает внимания. В ходе разработки я столкнулся с проблемой зацикливания команды GM: процесс постоянно пересобирал ветку MC и создавал новую сборку в TeamCity. Проблема оказалась в том, что TeamCity не успел скачать обновления репозитория, в которых была ветка MC, созданная процессом GM несколько секунд назад. К слову, интервал обновления репозитория в TeamCity у нас составляет примерно 30 секунд.

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

Кто-то посчитает решение очевидным, но я нашёл его не сразу. Оказывается, прикрепить ревизию к сборке при её добавлении в очередь можно при помощи параметра lastChanges метода addBuildToQueue:

<lastChanges>  <change    locator="version:{{revision}},buildType:(id:{{build_type}})"/></lastChanges>

В этом примере {{revision}} заменяется на 16-ричную последовательность коммита, а {{build_type}} на идентификатор конфигурации сборки. Но этого недостаточно, так как TeamCity, не имея информации о новом коммите, может отказать нам в запросе.

Для того чтобы новый коммит дошёл до TeamCity, нужно либо подождать примерно столько, сколько указано в настройках конфигурации корня VCS, либо попросить TeamCity проверить наличие изменений в репозитории (Pending Changes) при помощи метода requestPendingChangesCheck, а затем подождать, пока TeamCity скачает изменения, содержащие наш коммит. Проверка такого рода выполняется посредством метода getChange, где в changeLocator нужно передать как минимум сам коммит в качестве параметра локатора version. Кстати, на момент написания статьи (и кода) на странице ChangeLocator в официальной документации описание параметра version отсутствовало. Быть может, поэтому я не сразу узнал о его существовании и о том, что это 40-символьный 16-ричный хеш коммита.

Псевдокод:

teamCity.requestPendingChanges(buildType)attempt = 1while attempt <= 20:  response = teamCity.getChange(commit, buildType)  if response.commit == commit:    return True # Дождались  sleep(10)return False

О предельно высокой скорости слияний

У жадной стратегии есть недостаток на поиск ветки с ошибкой может потребоваться много времени. Например, 6 сборок для 20 тикетов у нас может занять около трёх часов. Можно ли устранить этот недостаток?

Допустим, в очереди находится 10 тикетов, среди которых только 6-й приводит к падению сборки.

Согласно жадной стратегии, мы пробуем собрать сразу все 10 тикетов, что приводит к падению сборки. Далее собираем левую половину (с 1 по 5) успешно, так как тикет с ошибкой остался в правой половине.

Если бы мы сразу запустили сборку на левой половине очереди, то не потеряли бы времени. А если бы проблемным оказался не 6-й тикет, а 4-й, то было бы выгодно запустить сборку на четверти длины всей очереди, то есть на тикетах с 1 по 3, например.

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

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

Примерно такой же алгоритм реализован в премиум-функции GitLab под названием Merge Trains. Перевода этого названия на русский язык я не нашёл, поэтому назову его Поезда слияний. Поезд представляет собой очередь запросов на слияние с основной веткой (merge requests). Для каждого такого запроса выполняется слияние изменений ветки самого запроса с изменениями всех запросов, расположенных перед ним (то есть запросов, добавленных в поезд ранее). Например, для трёх запросов на слияние A, B и С GitLab создаёт следующие сборки:

  1. Изменения из А, соединённые с основной веткой.

  2. Изменения из A и B, соединённые с основной веткой.

  3. Изменения из A, B и C, соединённые с основной веткой.

Если сборка падает, то соответствующий запрос из очереди удаляется, а сборки всех предыдущих запросов перезапускаются (без учёта удалённого запроса).

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

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

Но если преград человеческой мысли нет, то пределы аппаратных ресурсов видны достаточно отчётливо:

  1. Каждой сборке нужен свой агент в TeamCity.

  2. В нашем случае у сборки мобильного приложения есть порядка 15-100 сборок-зависимостей, каждой из которых нужно выделить по агенту.

  3. Сборки автомерджа мобильных приложений в main составляют лишь малую часть от общего количества сборок в TeamCity.

Взвесив все плюсы и минусы, мы решили пока остановиться на алгоритме SM + GM. При текущей скорости роста очереди тикетов алгоритм показывает хорошие результаты. Если в будущем заметим возможные проблемы с пропускной способностью, то, вероятно, пойдём в сторону Merge Trains и добавим пару параллельных сборок GM:

  1. Вся очередь.

  2. Левая половина очереди.

  3. Левая четверть очереди.

Что в итоге получилось

В результате применения комбинированной стратегии автомерджа нам удалось добиться следующего:

  • уменьшение среднего размера очереди в 2-3 раза;

  • уменьшение среднего времени ожидания в 4-5 раз;

  • мердж порядка 50 веток в день в каждом из упомянутых проектов;

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

Примеры графиков слияний за несколько дней:

Количество тикетов в очереди до и после внедрения нового алгоритма:

Среднее количество тикетов в очереди (AVG) уменьшилось в 2,5 раза (3,95/1,55).

Время ожидания тикетов в минутах:

Среднее время ожидания (AVG) уменьшилось в 4,4 раза (155,5/35,07).

Подробнее..

Вначале былworkflow

08.12.2020 14:22:23 | Автор: admin

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

Часть 1: Рабочий процесс

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

А далее как обычно бывает: всплывают первые запросы от пользователей на добавление новых фич/устранение багов ит.д., разработка кипит. Для того чтобы ускорить выход новых версий, принимается решение расширить командуDevOpsом, и для решения насущных проблемDevOpsпредлагает построитьCI/CD-конвейер (pipeline). И вот пришло время рассмотреть, как жеCI/CD-конвейерляжет на нашрабочий процесс,где у нас сейчас только мастер.

Для примера мы взяли простойконвейерс одним окружением. И вроде всёвыглядит хорошо: разработчик запушил код в мастер, запустилсяконвейер, код прошёл ряд проверок, собрался и развернулся в окружении.

А теперь рассмотрим ситуацию, когдаконвейерпрервался на тестах.

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

На данной картинке (которая может показаться слишком преувеличенным примером, однако такое бывает), мы видим,что в первом коммите, который ранее попал на окружение,каких-либо проблем нет.На втором коммите в мастерконвейерпрервался.И вот тут начинается самое интересное. Понятно, что запушенный код нерабочий и надо его исправлять, чем и занялся разработчик. Но что,еслиу нас не один разработчик, а команда, где каждый усердно трудится над своей задачей? Второй разработчик ответственно начал добавлять новые улучшения в продукт, но в их основе лежит второй коммит. Что же будет дальше с этими изменениями? Сколько времени уйдёт у первого разработчика на исправление? Насколько сильными будут изменения в новом коммите? Что в это время делать второму разработчику? Что делать с уже написанными вторым разработчикомфичами? В общем, слишком много вопросов, а на выходе получаем:

  • уменьшение производительности,

  • впустую потраченное время,

  • много головной боли.

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

Первым делом добавимнебезызвестныеfeature-ветки.

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

И в очередной разпроиграемпроблему: вfeature-ветке обнаружен баг.

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

Но что,еслина окружение попал новый мастер,и спустя какое-то время обнаружен баг (не углядели, всякое бывает).

Соответственно, это уже критическая ситуация: клиент не доволен, бизнес не доволен. Нужно срочно исправлять! Логичным решением будет откатиться. Но куда? Заэтовремя мастер продолжал пополняться новыми коммитами. Даже если быстро найти коммит,в котором допущена ошибка,и откатить состояние мастера, то что делать с новыми фичами, которые попали в мастер после злосчастного коммита? Опять появляется много вопросов.

Что ж, давайте не будем поддаваться панике,и попробуем ещёраз изменить нашрабочий процесс,добавив теги.

Теперь,когда мастерпополняетсяизменениями изfeature-веток, мы будем помечать определённое состояниемастера тегом.

Но вот в очередной раз пропущен баг в тегеv2.0.0, который уже на окружении.

Как решить проблему теперь?

Правильно, мы можем повторно развернуть версиюv1.0.0, считая её заведомо рабочей.

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

  • сэкономили время и,как следствие,деньги,

  • восстановили работоспособность окружения,

  • предотвратили хаос,

  • локализовали проблему в версииv2.0.0.

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

Для примера возьмём и рассмотримдавновсем известныйGitFlow:

Сравним его с нашим последним примером иувидим,что у нас нетdevelop-ветки, а ещёмы не использовалиhotfixes-ветки. Следовательно,мы не можем сказать, что использовали именноGitFlow. Однако мы немного изменим наш пример, добавивdevelop-иrelease-ветки.

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

Что ж, наGitFlowжизнь не заканчивается, ведь есть не менее известныйGitHubFlow.

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

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

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

Часть 2: Участь DevOps'а

В первой части мы рассмотрели, как выглядитрабочий процесс, а теперь посмотрим, почему для DevOps-инженератак важен корректно настроенный рабочий процесс.Для этого вернёмся к последнему примеру,аименно к построению того самогоконвейерадля реализации процесса CI/CD.

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

Собственно, построениеконвейераможно изобразить вот такой простой картинкой:

Ну или одним вопросом: как связать между собой код в репозитории и окружение?

Следовательно, нужно понимать,какой именно код должен попасть в окружение, а какой нет. К примеру, еслив ответна вопрос: Какойрабочий процессиспользуется? мы услышим: GitHubFlow, то автоматически мы будем искать нужный код вmaster-ветке. И ровно наоборот, если не построен никакойрабочий процесси куски рабочего кода разбросаны по всему репозиторию, то сначала нужно разобраться срабочим процессом, а лишь потом начинать строитьконвейер.Иначе рано или поздно на окружение попадёт то, что возможно не должно там быть, и как следствие,пользователь останется без сервиса/услуги.

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

Нодля наглядностидалее рассмотрим два основных этапа вCI/CD- конвейерах: build и deployment/delivery. И начнем мы,пожалуй,с первогоbuild.

Buildпроцесс, конечным результатом которого является артефакт.

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

Так вот, у нас есть отличная возможность взять имя тега для артефакта и опубликовать его. Но что,если у нас нет никакогорабочего процесса?Что ж, тут уже сложнее. Конечно, мы можем взятьхешкоммита,или дату, или придумать что-либо ещёдля идентификации артефакта.Но очень скороразобраться в этом будет практически невозможно.

И вот пример из реальной жизни.

Представьте ситуацию, когда вы хотите загрузить новую версиюUbuntu, и вместо такого списка версий:

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

Бываютслучаи, когда мы можем пренебречь именованием. Поэтому рассмотрим ещёодин небольшой пример:у нас нет конкретногорабочего процесса;как следствие,у нас нет понимания,что именно мы должны хранить в нашем хранилище. Что,в свою очередь,может бытьчреватопоследствиями, так как хранилище так или иначе ограничено: либо деньгами, либо местом, либо и тем,и другим. Поэтому в ситуации, когда у нас нет конкретногорабочего процесса,мы можем начать публиковать артефакт из каждойfeature-ветки (так как чёткой определённости у нас нет), но в таком случае рано или поздно возникнет ситуация, когда ресурсы закончатся, и придётся расширяться, что опять же несёт за собой трату как человеческих, так и денежных ресурсов.

Конечно,на этом примеры не заканчиваются, но думаю,чтотеперь мы можем перейти к delivery/deployment.

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

Deploymentпроцесс,в рамках которого развёртка приложения происходит автоматически.

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

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

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

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

Заключение

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru