[ELMA3] Асинхронные операции в сценариях

Внимание!

Данная статья актуальна только для версий системы 3.15.0 и выше.

В версии системы 3.15.0 улучшено масштабирование среды исполнения процессов за счёт возможности использовать фоновые операции (ФО) в пользовательских сценариях. Созданы реализации фоновых операций: HTTP запрос и Получить BinaryFile.

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

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

  1. Вычислительная работа, когда активно используется процессор.
  2. Операции ввода-вывода (I/O), когда процессор не используется, а поток простаивает в ожидании завершения операции. Такое поведение ещё называется синхронным вводом-выводом. При большом количестве работающих процессов, где часто встречаются сценарии с синхронным I/O, в пуле могут закончиться свободные потоки и тогда начнёт копиться очередь исполнения. Кроме того, длительные ожидания могут привести к превышению ограничения на время выполнения активности и к её перезапуску.

Фоновые операции призваны избавиться от простоя потока при I/O операции за счёт применения асинхронных операций ввода-вывода. Тогда они уже не будут блокировать исполняющий поток, позволяя продолжить выполнение кода. К тому же их можно не ограничивать по времени выполнения, как стандартные активности процессов.

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

ФО делит всю работу сценария на 3 этапа.

Первый этап. До операции

Подготавливаются входные данные для фоновой операции, и она создаётся при помощи билдеров-конфигураторов, которые опубликованы в PublicAPI и располагаются в разделе PublicAPI.Processes.BackgroundOperations. Затем созданный объект возвращается методом сценария – и здесь требуется заменить тип возвращаемого значения с void на интерфейс IBackgroundOperation, от которого происходят все ФО. На этом работа блока сценария приостанавливается, поскольку для продолжения требуется результат операции.

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

Ещё несколько слов о создании ФО. У фоновых операций третий этап может отсутствовать. Но чаще всего его все же приходится планировать, поскольку требуется обработать результат. Это самое продолжение "после операции" указывается на этапе создания ФО при помощи делегатов обратного вызова. Делегатом может являться как метод, так и функция, возвращающая IBackgroundOperation. Первым параметром любого делегата всегда является контекст процесса. Второй параметр зависит от операции (его может и не быть) и от типа делегата.

Делегаты бывают 2 видов:

  1. Для успешного завершения – для обработки полученного результата. Как правило, его второй параметр – это результат работы фоновой операции. Например, для операции чтения файла — это будет массив прочитанных байт, для http запроса – ответ от удалённого сервера и т.п.
  2. Для неудачного завершения – для обработки ошибки при выполнении ФО. Обычно, второй параметр – это исключения, возникшие в ходе выполнения ФО.

Второй этап. Фоновая операция

Запускается фоновая операция. При запуске у ФО устанавливается время истечения. Пока операция работает, эта временная отметка регулярно сдвигается. Но если операция истекает, то она перезапускается. Это позволяет защититься от сбоев сервера или их внезапного отключения. После завершения ФО её результат будет сохранён в БД.

На время работы второго этапа ФО никаких транзакций не открывается, контекст процесса не требуется.

Третий этап. После операции

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

Третий этап может создать и вернуть новую ФО.

Порядок обработки ошибок

При выполнении ФО (этап 2) могут возникнуть ошибки. Их порядок обработки определён следующим образом:

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

Порядок обработки ошибок, возникших на этапах 1 и 3:

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

Требования к делегатам

  1. Делегат должен быть объявлен как публичный.
  2. Делегат не может быть анонимной лямбдой.

Реализация новых фоновых операций

Для реализации новой ФО требуется:

  1. Создать класс-наследник от BaseBackgroundOperationWithCallbacks. Этот базовый класс сразу поддерживает оба делегата обратного вызова. Если делегаты не нужны (выполнение по принципу "запустил и забыл"), то лучше создать новый базовый класс, унаследованный от IBackgroundOperation.
  2. Сделать свой класс сериализуемым – указать атрибут [Serializable].
  3. Создать билдеры для делегатов, билдер-конфигуратор для самой фоновой операции и зарегистрировать его в PublicAPI.
  4. Создать исполнителя ФО через реализацию компонента точки расширения IBackgroundOperationExecutor. Для этого создать свой класс от абстрактного исполнителя ФО BackgroundOperationExecutorBase.
  5. Реализовать методы из базового класса (приведены ниже). Ваш компонент из метода RunTask должен возвращать уже запущенную асинхронную задачу.
/// <summary>
/// Может ли компонент выполнить фоновую операцию?
/// </summary>
/// <param name="executionInfo">Данные для выполнения операции</param>
/// <returns>true – может, false – не сможет</returns>
public abstract bool CanExecute(IBackgroundOperation executionInfo);

/// <summary>
/// Получить описание фоновой операции (какая операция выполняется)
/// </summary>
/// <param name="executionInfo">Данные об операции</param>
/// <returns>Описание фоновой операции (какая операция выполняется)</returns>
public abstract string GetDescription(IBackgroundOperation executionInfo);

/// <summary>
/// Реализация запуска фоновой операции для классов-наследников
/// </summary>
/// <param name="executionInfo">Данные об операции</param>
/// <param name="token">Токен отмены</param>
/// <returns>Системная задача</returns>
protected abstract Task<object> RunTaskImpl(IBackgroundOperation executionInfo, CancellationToken token);

Процедура отмены

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

Поддержку отмены в асинхронных задачах иметь желательно, но необязательно. К примеру, многие асинхронные операции I/O не могут быть отменены в C#.

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

Заключение

Подведём итог. Фоновые операции обладают следующими свойствами:

  1. Они не ограничиваются по времени выполнения, как обычные элементы очереди исполнения. Длительность может быть любой. Если сценарий, содержащий долгий запрос во внешнюю систему, отваливается с ошибкой таймаута транзакции, то, реализовав это поведение через фоновую операцию, мы устраним подобные ошибки.
  2. Они не блокируют поток выполнения ожиданием результата, а позволяют продолжить обработку других элементов очереди исполнения Workflow в нем, пока операция ввода-вывода работает в фоне. Это увеличивает пропускную способность среды исполнения процессов. Одновременно с этим повышается утилизация ресурсов сервера, ведь теперь время ожидания завершения I/O заменено на полезную работу над другими элементами очереди исполнения.
  3. Фоновые операции базируются на системных классах Threading.Task. Внутри каждой ФО создаётся хотя бы одна асинхронная задача (Task).
  4. Отмена ФО не является мгновенной. С момента запроса на отмену и до фактической отмены может пройти до 10 секунд.

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

  1. Добавить using-и в сценарий.
  2. Изменить возвращаемое значение метода, который вызовется при запуске сценария, с voidна IBackgroundOperation.
  3. Добавить делегаты для успешного завершения и для неудачного завершения.
  4. Создать объект фоновой операции в теле метода и вернуть его.

Фоновая операция "HTTP запрос" в сценарии

Для создания фоновых операций HTTP запрос сделан каскад билдеров-конфигураторов операции:

  1. Первый каскад – это указание обязательных параметров для создания HTTP запроса: адрес, HTTP метод и отправляемые данные, если метод подразумевает отправку данных.
  2. Второй каскад даёт возможность указать необязательные параметры HTTP запроса: дополнительные заголовки, делегаты обратного вызова после выполнения операции и, потенциально, иные параметры (например, клиентские сертификаты, параметры авторизации и т.п.). Завершается построение фонового HTTP запроса всегда методом Create.

Начальный билдер доступен через PublicAPI: PublicAPI.Processes.BackgroundOperations.HttpRequestBuilder.

В данный момент на первом каскаде доступны предопределённые HTTP методы:

  • HEAD;
  • GET;
  • DELETE;
  • POST с отправляемыми данными;
  • PUT с отправляемыми данными.

Также есть метод, который позволяет указать свой HTTP метод.

Во втором каскаде есть свойство Headers – это коллекция заголовков HTTP.

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

Метод Set устанавливает новое значение. Если заголовок уже существует, то его значение заменяется на новое.

Также во втором каскаде присутствует универсальный метод WhenCompleted для установки делегатов обратного вызова. С этим методом обязательно указывается тип контекста процесса. Параметрами метода являются делегаты-методы с параметром билдера-конфигуратора делегатов обратного вызова:

public ITerminatingBuilder WhenCompleted<TContext>(
    Action<HttpSuccessDelegateBuilder<TContext>> onSuccess,
    Action<HttpErrorDelegateBuilder<TContext>> onError)
У этих билдеров-конфигураторов определены 3 возможных операции: 
/// <summary>
/// Не вызывать делегат и не обрабатывать результат
/// <para>
/// Учтите, что если Вы выбираете данное поведение в качестве реакции на неудачное завершение операции,
/// то при возникновении ошибки у Вас либо сработает эскалация по коннектору для ошибок в элементе,
/// либо будет зарегистрирована ошибка и начнутся попытки повторного выполнения операции
/// </para>
/// </summary>
public void DoNotHandle()

/// <summary>
/// Для обработки результата операции вызвать указанную функцию, которая к тому же может вернуть новую фоновую операцию
/// <para>
/// НЕЛЬЗЯ передавать делегат, являющийся анонимной лямбдой!
/// </para>
/// </summary>
/// <param name="func">Функция обработки результата фоновой операции</param>
/// <exception cref="ArgumentException">Делегат обратного вызова не может являться анонимной лямбдой.</exception>
public void CallFunc(Func<TContext, T, IBackgroundOperation> func)

/// <summary>
/// Для обработки результата операции вызвать указанный метод
/// <para>
/// НЕЛЬЗЯ передавать делегат, являющийся анонимной лямбдой!
/// </para>
/// </summary>
/// <param name="method">Метод обработки результата фоновой операции</param>
/// <exception cref="ArgumentException">Делегат обратного вызова не может являться анонимной лямбдой</exception>
public void CallMethod(Action<TContext, T> method)

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

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

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

Следует помнить, что делегаты обратного вызова могут снова создать и вернуть новую фоновую операцию.

Пример использования в сценарии:

// подключить именованные области
using EleWise.ELMA.Model.BackgroundOperations;
using EleWise.ELMA.Workflow.Models.BackgroundOperations.HttpRequest;

public partial class P_HttpGet_Scripts : EleWise.ELMA.Workflow.Scripts.ProcessScriptBase<Context>
{
    public virtual IBackgroundOperation TestBackgroundOperation(Context context)
    {
        return PublicAPI.Processes.BackgroundOperations
            .HttpRequestBuilder(context.URL)
            .Get()
            .WhenCompleted<Context>(
                onSuccess => onSuccess.CallMethod(SuccessMethod),
                onError => onError.CallMethod(ErrorMethod))
            .Create();
    }

    // делегат для успешного завершения
    public void SuccessMethod(Context context, HttpResponse response)
    {
        context.Result = response.Body.GetUtf8String();
    }

    // делегат для неудачного завершения
    public void ErrorMethod(Context context, Exception exception)
    {
        context.Result = exception.Message;
        context.StackTrace = exception.ToString();
    }
}

Пример создания ФО с запросом HTTP POST:

public virtual IBackgroundOperation HttpPostBackgroundOperation(Context context)
{
    var json = "{ \"index\": 3, \"value\": \"data\"}";

    return PublicAPI.Processes.BackgroundOperations
        .HttpRequestBuilder(context.URL)
        .PostJson(json)
        .WhenCompleted<Context>(
            onSuccess => onSuccess.CallMethod(SuccessMethod),
            onError => onError.CallMethod(ErrorMethod))
        .Create();
} 

Фоновая операция "Получить BinaryFile" в сценарии

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

Билдер ФО доступен через Public API: PublicAPI.Processes.BackgroundOperations.FetchBinaryFileBuilder.

Для создания ФО необходимо указать идентификатор файла (Id или Uid в строковом представлении) и делегаты обратного вызова. При успешной работе операция вернёт объект BinaryFile, содержимое которого может оказаться закешированным в памяти. В файле настроек Settings.config есть переменная BackgroundOperation.FetchBinaryFile.MaxSizeToPrecache, которая позволяет управлять, какой максимальный размер файла разрешается кешировать.

Пример использования:

using EleWise.ELMA.Files;
using EleWise.ELMA.Model.BackgroundOperations;

public partial class P_BinaryFile_Scripts : EleWise.ELMA.Workflow.Scripts.ProcessScriptBase<Context>
{
    public virtual IBackgroundOperation OnLoadFile (Context context)
    {
        return PublicAPI.Processes.BackgroundOperations.FetchBinaryFileBuilder(context.FileUid.ToString())
            .WhenCompleted<Context>(
                onSuccess => onSuccess.CallMethod(ProcessBinaryFileContent),
                onError => onError.CallMethod(HandleError))
            .Create();
    }

    public void ProcessBinaryFileContent(Context context, BinaryFile binaryFile)
    {
        // обрабатываем контент полученного binaryFile
        // ...
    }

    public void HandleError(Context context, Exception exception)
    {
        // обрабатываем ошибку
    }