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

Ранее мы рассмотрели роль СМЭВ в обеспечении работоспособности портала «Госуслуг», а также общие принципы организации взаимодействия с ним на стороне поставщика сведений посредством 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 для целей интеграции с порталом «Госуслуг» является работоспособным решением. А некоторые нюансы его использования, которые мы рассмотрели, могут помочь сократить время на реализацию проектов «с нуля».

Let’s block ads! (Why?)

Read More

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *