Сама по себе эта статья не является самостоятельным произведением. По факту она служит дополнением к основной, обзорной, статье по новой библиотеке ActiveSession. Эта библиотека предназначена для использования в веб-приложениях, серверная часть которых написана на ASP.NET Core. В основной статье рассказано, какую задачу решает эта библиотека, и как подключить ее к приложению. А эта статья рассказывает более подробно, как использовать эту библиотеку в обработчиках запросов к серверной части приложения из браузера.
Если вы уже заинтересовались библиотекой ActiveSession, то добро пожаловать дальше. Только перед этим рекомендуется прочитать основную статью: в ней объяснены те понятия, которые нужны для лучшего понимания того, что написано в этой статье.


Содержание

Введение



Работа с библиотекой ActiveSession



Классы стандартных исполнителей библиотеки ActiveSession



Заключение



Введение



А что на картинке?

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


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

А вот жизненный цикл исполнителя для данного примера — другой.


  1. Обработчик запроса POST на сервере создает исполнитель (вызовом IActiveSession.CreateRunner), запускает его фоновый процесс и возвращает начальный результат (всё это — вызовом метода IRunner.GetRequiredAsync). Обработчик запроса формирует страницу, отображающую этот результат, и возвращает ее браузеру. Фоновый процесс исполнителя при этом продолжает выполняться на сервере, а инфраструктура библиотеки ActiveSession удерживает в своем хранилище ссылку на этот исполнитель.
  2. Браузер отображает полученную страницу и выполняет размещенный на ней сценарий, подгружающий дополнительные данные.
  3. Сценарий с определенным интервалом делает запросы к точке вызова API на сервере с использованием fetch().
  4. Обработчик точки вызова API находит исполнитель, запрашивает у него текущий результат вместе с состоянием и отправляет их сценарию в браузере в качестве ответа.
  5. Сценарий, получив ответ, изменяет содержимое страницы, чтобы отобразить полученный результат.
  6. После первых двух полученных ответов сценарий обнаруживает, что исполнитель ещё выполняется, и планирует следующий запрос к точке вызова API сервера.
  7. После получения второго ответа пользователь увидел, что ему нужно, и послал команду на прекращение выполнения исполнителя (например, нажал на кнопку на странице). Эта команда была преобразована сценарием на странице в обращение к соответствующей точке вызова API.
  8. Обработчик точки вызова API нашел исполнитель и вызвал метод прерывания его работы. Исполнитель в результате вызова этого метода перешел в завершающую стадию. Инфраструктура библиотеки ActiveSession, обнаружив завершение исполнителя, удаляет ссылку на него из хранилища. Теперь объект исполнителя может быть утилизирован сборщиком мусора.
  9. После отправки команды прекращения работы исполнителя сценарий отменил периодические запросы к точке вызова API сервера.

То есть, пользователь в этом примере не стал дожидаться окончания фонового выполнения исполнителя, а послал команду прервать его выполнение. И обработчик точки вызова API прервал выполнение.



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


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


  1. Документация по API библиотеки: интерфейсы, классы, методы.
  2. Исходный код проекта, откуда взято большинство примеров.



Работа с библиотекой ActiveSession



Получение доступа к активному сеансу из обработчика запроса


Внутри обработчиков запросов вся работа с активным сеансом и выполняемыми в нем исполнителями ведется через интерфейс IActiveSession объекта активного сеанса. Чтобы получить ссылку на этот интерфейс, нужно использовать метод расширения GetActiveSession() для объекта контекста запроса (типа HttpContext). А как именно получить доступ к контексту запроса в обработчике — это зависит от используемого обработчиком фреймворка. В обработчики, реализованные в рамках базового фреймворка ASP.NET Core как компоненты конвейера или конечные точки маршрутизации, контекст запроса передается непосредственно, как аргумент. В методы-обработчики на базе Minimal API можно добавить параметр типа HttpContext и Minimal API привяжет к нему контекст обрабатываемого запроса. В фреймворках MVC и RazorPages контекст запроса сохраняется в свойстве HttpContext базового для обработчиков класса в соответствующем фреймворке — ControllerBase или PageModel соответственно. Для работы с приложениями на базе SignalR или фреймворка Blazor библиотека ActiveSession не предназначена: там используются другие подходы к сохранению состояния приложения.
Пример получения активного сеанса приведен конце следующей главы вместе с прочими примерами работы с активным сеансом.



Работа с активным сеансом.


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



Создание нового исполнителя.


Новый исполнитель создается методом CreateRunner<TRequest,TResult>(TResult, HttpContext) интерфейса IActiveSession. Этот обобщенный метод имеет два параметра-типа: TRequest — тип первого параметра метода, и TResult — тип результата для создаваемого исполнителя. Значение первого параметра метода CreateRunner передается в метод Create подходящей фабрики исполнителя и используется при создании нового исполнителя. Второй параметр — контекст запроса, в обработчике которого создается исполнитель.


Этот метод возвращает обобщенную структуру KeyedRunner<TResult> с двумя полями: IRunner<TResult> Runner, которое содержит ссылку на вновь созданный исполнитель, и int RunnerNumber, которое содержит уникальный в пределах сеанса номер созданного исполнителя.


Ни один из параметров, переданных этому методу, не зависит от второго параметра-типа, поэтому параметры-типы этого метода не могут быть выведены компилятором и должны быть указаны явно. Кроме того, тип результата стандартного исполнителя может быть довольно громоздким. Из-за этих неудобств библиотека ActiveSession определяет ряд методов расширения для интерфейса IActiveSession, которые создают стандартные исполнители библиотеки ActiveSession. Для каждого из этих методов есть несколько перегруженных вариантов. Хотя эти методы тоже являются обобщенными, их использовать удобнее, чем CreateRunner: у них есть только один параметр-тип (такой же, как параметр-тип класса создаваемого стандартного исполнителя), который к тому же часто можно вывести из их параметров. Кроме того, они упрощают указание типа результата, используя параметр-тип класса стандартного исполнителя для определения фактического типа результата созданного исполнителя. Вот эти методы:


  • CreateSequenceRunner<TItem> — создает, в зависимости от типов используемых параметров, исполнитель типа EnumAdapterRunner<TItem> или AsyncEnumAdapterRunner<TItem>;
  • CreateTimeSeriesRunner<TResult> — создает исполнитель типа TimeSeriesRunner<TResult>;
  • CreateSessionProcessRunner<TResult> — создает исполнитель типа SessionProcessRunner<TResult>;

Эти методы возвращают ту же обобщенную структуру KeyedRunner<TResult> с соответствующим образом установленным параметром-типом, что и метод CreateRunner.



Получение существующего исполнителя


Обобщенный метод интерфейса IActiveSession GetRunner<TResult>(int, HttpContext) с одним параметром-типом TResult используется, чтобы получить ссылку на существующий исполнитель с типом результата TResult, который выполняется в текущем активном сеансе и зарегистрирован под номером исполнителя, переданным в качестве первого параметра. Если не существует исполнителя, зарегистрированного под указанным номером, или если зарегистрированный исполнитель имеет несовместимый тип результата, метод GetRunner возвращает null.


Параметры обобщенного метода GetRunner не зависят от его параметра-типа, поэтому параметр-тип не может быть выведен и должен быть указан.


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


  • GetSequenceRunner<TItem> — находит стандартный исполнитель классов EnumAdapterRunner<TItem> или AddAsyncEnumAdapter<TItem> (эти исполнители имеют одинаковый тип результата);
  • GetTimeSeriesRunner<TResult> — находит стандартный исполнитель класса TimeSeriesRunner<TResult>.

Для нахождения исполнителя класса SessionProcessRunner дополнительный метод расширения не требуется: параметр-тип этого класса совпадает с параметром-типом самого метода GetRunner (т.е. типом результата исполнителя).


метод GetRunnerAsync

В интерфейсе IActivesession определена также асинхронная версия метода для нахождения существующего исполнителя, GetRunnerAsync:


ValueTask<IRunner<TResult>?> GetRunnerAsync<TResult>(
    int RunnerNumber,
    HttpContext Context, 
    CancellationToken CancellationToken = default
);

В отличие от синхронной версии метод GetRunnerAsync принимает дополнительный параметр CancellationToken, дающий возможность отмены операции поиска, а возвращает этот метод значение ValueTask<IRunner<TResult>?> — задачу, результатом которой является найденный исполнитель (или null). В нынешнюю версию библиотеки этот метод добавлен "на вырост": он предназначен для нахождения исполнителей на удаленных узлах, а в нынешней версии реализация этой функции — пока что только в планах. Ну, а для локальных исполнителей он всегда выполняется синхронно и возвращает завершенную задачу.


Пару методов GetRunner/GetRunnerAsync интерфейса активного сеанса дополняет аналогичная им пара типонезависимых методов GetNonTypedRunner/GetNonTypedRunnerAsync. Эти методы имеют те же параметры (кроме параметра-типа), что и, соответственно, GetRunner и GetRunnerAsync, но возвращают они базовую, типонезависимую часть интерфейса исполнителя: IRunner. Эти методы полезны для использования в обработчиках запросов, которые не работают с результатами, а вызывают лишь методы этого базового, типонезависимого интерфейса исполнителя. Например — прекращают выполнение исполнителя, вызывая метод базового интерфейса Abort (подробнее об этом методе см. далее, в описании работы с исполнителями).



Прекращение активного сеанса.


Метод Terminate интерфейса активного сеанса IActiveSession содержит завершает этот активный сеанс. Завершение активного сеанса приводит к прекращению выполнения всех связанных с ним исполнителей (для каждого из их вызывает метод Abort). После этого активный сеанс удаляется из хранилища, очищаются все связанные с ним ресурсы и любая работа с прекращенным сеансом в этом запросе (например, запуск новых исполнителей) становится невозможной.


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


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



Связывание данных с активным сеансом и отслеживание завершения и очистки активного сеанса.


Свойство Properties интерфейса активного сеанса IActiveSession позволяет связывать произвольные объекты с активным сеансом. Это свойство представляет собой словарь со строкой в качестве ключа и произвольным объектом в качестве значения. Объекты можно добавлять с соответствующими им ключами и извлекать по этим ключам.


Локальность свойства Properties

В текущей версии библиотеки это не важно, но другое отличие сохраняемых в Properties объектов от значений сохраняемых в сеансе ASP.NET Core — в том, что эти объекты локальны для каждого узла, на котором выполняется приложение, использующее библиотеку ActiveSession. При использовании последующих версий библиотеки, которые смогут поддерживать активные сеансы, распространяющиеся на несколько узлов, это отличие может стать существенным.


Если какие-либо объекты, добавленные в Properties, являются очищаемыми (т. е. реализуют интерфейсы IDisposable и/или IAsyncDisposable), то, чтобы выполнить надлежащую очистку этих связанных с сеансом объектов, следует отслеживать завершение и очистку активного сеанса. В жизненном цикле активного сеанса есть два события, которые можно отслеживать. Во-первых, свойство CompletedToken интерфейса IActiveSession содержит маркер отмены (CancellationToken), который будет отменен, когда активный сеанс завершится и будет готов к очистке. Во-вторых, свойство CleanupCompletionTask содержит задачу, которая завершается, когда активный сеанс завершит очистку. Ссылку на эту же самую задачу возвращает и метод Terminate, описанный ранее.



Идентификация активного сеанса.


Для выделения конкретного активного сеанса среди всех, доступных в текущий момент, служит свойство String Id.
Для выделения конкретного активного сеанса среди всех не только существующих в данный момент, но и ранее существовавших, к в дополнение к свойству Id требуется свойство int Generation.


Подробности

Свойство Id (идентификатор) активного сеанса — строка, состоящая из основной части и, возможно, суффикса. Основная часть идентификатора — одинаковая для всех активных сеансов в сеансе ASP.NET Core, представляемом интерфейсом ISession. Ее значение хранится в переменных этого сеанса (когда-то она просто совпадала с ISession.Id, но потом по соображениям безопасности получила отдельное значение. Суффикс предназначен для различения разных активных сеансов, связанных с одним сеансом ASP.NET Core. В текущей версии библиотеки суффикс не используется, так что с одним сеансом ASP.NET Core в данной версии может быть связан только один существующий на текущий момент активный сеанс.


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


Свойство Generation содержит номер поколения активного сеанса: которым по счету был создан этот активный сеанс среди сеансов c тем же Id. Значение этого свойство, наряду со значением свойства Id, составляет идентификатор активного сеанса, входящий во внешний идентификатор исполнителя (о нем см. далее в этой главе).



Внешний идентификатор исполнителя для клиентской части веб-приложения


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


Для выполнения этой задачи в библиотеке ActiveSession определен класс ExtRunnerKey. Помимо номера исполнителя, он содержит данные, которые однозначно идентифицируют активный сеанс. Метод IsForSession(IActiveSession) экземпляра этого класса проверяет, является ли активный сеанс, переданный в качестве аргумента, тем же самым, для которого был создан этот экземпляр.


Существует два способа использования внешнего идентификатора исполнителя (ExtRunnerKey). Первый способ — передать его значение обратно на сервер как объект. Объект может быть передан как целое в параметрах вызова API, будучи преобразованным в JSON или XML. Или он может быть передан как набор значений формы с соответствующими именами и привязан к параметру обработчика, имеющему тип ExtRunnerKey, через процесс привязки параметров MVC или Minimal API. Этот метод передачи внешнего идентификатора исполнителя хорошо подходит для обработки вызовов API из скриптов, выполняемых на странице в браузере, и для обработки ввода из форм.


Другой способ передачи значения ExtRunnerKey — преобразование его в строку через ExtRunnerKey.ToString(). Этот метод хорошо подходит для передачи внешнего идентификатора исполнителя как части пути URL или значений запроса URL, поскольку преобразованное значение не содержит символов, которые не могут использоваться в этих частях URL. Переданное значение может быть преобразовано обратно с помощью статического метода ExtRunnerKey.TryParse.



Другие методы и свойства интерфейса активного сеанса


убрано в скрытый текст

Свойство SessionServices содержит ссылку на контейнер сервисов для ограниченной области, связанной с данным активным сеансом. Эта область существует, пока существует активный сеанс. Контейнер, на который ссылается это свойство, предназначен для получения сервисов с временем жизни ограниченной области (Scoped), которые можно использовать в исполнителях, работающих в этом активном сеансе — либо используя свойство SessionServices в качестве Service Locator, либо путем внедрения зависимостей через предназначенные для этого сервисы-адаптеры (они будут рассмотрены в следующей главе). А так как внедрение зависимостей является предпочтительным способом работы с контейнером сервисов, то прямая ссылка на контейнер сервисов активного сеанса не предназначена для широкого использования, а потому информация о ней убрана в скрытый текст.


Свойство IsFresh устанавливается в true при создании активного сеанса и сбрасывается в false при создании в нем первого исполнителя. Его можно использовать в обработчике текущего запроса для того, чтобы узнать, исполняются ли сейчас и исполнялись ли вообще в текущем активном сеансе какие-либо исполнители.


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



Примеры работы с активным сеансом


Получение ссылки на активный сеанс, проверка его доступности и создание исполнителя в обработчике страницы Razor Pages

Pages\SequenceAdapterParams.cshtml.cs


        //Come here after the data are entered to the form on the page 
        //(the page template is beyond the scope of the example)
        public ActionResult OnPost() 
        {
            if(ModelState.IsValid) { 
                //Make some input processing
                // IEnumerable<SimSeqData> sync_source;
                //...

                //Obtain a reference on the active session object for this request
                IActiveSession session= HttpContext.GetActiveSession();

                //Check that the active session is available
                if(session.IsAvailable) {

                    //Create a new runner
                    (var runner, int runner_number)= session.CreateSequenceRunner(sync_source, HttpContext);

                    //This part will be explained later  
                    //in an external runner identifier usage example
                    ExtRunnerKey key = (session, runner_number); //Make an external identifier
                    //Pass the external identifier by a part of the redirection URL path
                    return RedirectToPage("SequenceShowResults", new { key }); 
                }
                else //An active session is unavailable 
                    return StatusCode(StatusCodes.Status500InternalServerError);
            }
            //Repeat input due to invalid data entered
            return Page();
        }

Прекращение активного сеанса в методе действия контроллера API MVC

APIControllers\SampleController.cs


    [HttpPost("[action]")]
    public IActionResult TerminateSession()
    {
        IActiveSession session = HttpContext.GetActiveSession();
        if(session.IsAvailable) {
            session.Terminate(HttpContext);
            return StatusCode(StatusCodes.Status204NoContent);
        }
        else return StatusCode(StatusCodes.Status500InternalServerError);
    }

Передача внешнего идентификатора исполнителя в форме объекта и поиск типонезависимого интерфейса этого исполнителя

В примере производится прекращение выполнения исполнителя связанного со страницей HTML, сформированной с помощью фреймворка Razor Pages.


1.Внешний идентификатор исполнителя сохраняется в поле _key класса модели страницы, доступном для соответствующего шаблона Razor Page (код сохранения не показан).


Pages\TimeSeriesResults.cshtml.cs


    public class TimeSeriesResultsModel : PageModel
    {
        internal ExtRunnerKey _key;
        }
        //...
    }

2.С помощью шаблона Razor Page на основе значения вышеупомянутого поля класса модели страницы устанавливается значение глобальной переменной JavaScript в результирующей HTML-странице, чтобы она содержала внешний идентификатор связанного с ней исполнителя. Затем внешний идентификатор исполнителя передается из обработчика выгрузки HTML-страницы конечной точке API Abort для завершения связанного исполнителя.


Pages\TimeSeriesResults.cshtml


@page "{key}"
@model SampleApplication.Pages.TimeSeriesResultsModel
<!-- ... -->

<script>
    var pollInterval = @Model._timeoutMsecs;
    var runner_key = { 
        RunnerNumber: @Model._key.RunnerNumber,
        Generation: @Model._key.Generation,
        _ActiveSessionId: "@(WebUtility.UrlEncode( Model._key.ActiveSessionId))",
        get ActiveSessionId() { return decodeURI(this._ActiveSessionId); }
    }, 

    window.onunload = function () {
        let request = {
            RunnerKey: runner_key,
        }
        fetch("@Model._AbortEndpoint", {
            method: "POST",
            headers: {
               "Content-type": "application/json;charset=utf-8"
            },
            keepalive: true,
            body: JSON.stringify(request)
        });
    }
</script>

3.Обработчик (метод действия контроллера API MVC) проверяет, работает ли он в том же активном сеансе, в котором был создан исполнитель. Если это так, то обработчик получает типонезависимый интерфейс этого исполнителя и прекращает его выполнение вызовом метода Abort полученного интерфейса. Использование типонезависимого интерфейса позволяет применять один и тот же обработчик API при закрытии разных страниц, с которыми могут быть связаны исполнители с разными типами результата.


APIControllers\SampleController.cs


    [HttpPost("[action]")]
    public ActionResult<AbortResponse> Abort(AbortRequest Request)
    {
        IActiveSession session = HttpContext.GetActiveSession();
        if(session.IsAvailable && Request.RunnerKey.IsForSession(session)) {
            var runner = session.GetNonTypedRunner(Request.RunnerKey.RunnerNumber, HttpContext);
            if(runner!=null) {
                AbortResponse response = new AbortResponse();
                response.runnerStatus=runner.Abort(HttpContext.TraceIdentifier).ToString();
                return response;
            }
        }
        return StatusCode(StatusCodes.Status410Gone);
    }

Передача внешнего идентификатора исполнителя как части пути в URL страницы и получение ссылки на исполнитель (его полный интерфейс) с использованием Razor Pages

В примере производится формирование передача следующей странице Razor Pages внешнего идентификатора исполнителя, с которым эта страница должна работать дальше. Идентификатор передается в виде части пути в URL для следующей страницы — сегмента для переменной с именем key, описанного в директиве page страницы.


1.Внешний идентификатор исполнителя преобразуется в строковое значение с целью передачи его в URL следующей страницы(это делается методом PageModel.RedirectToPage()).


Pages\SequenceAdapterParams.cshtml.cs


        //This is part of the previous example from "Creating a new runner" section
        public ActionResult OnPost() 
        {
            //...
                    //Create a new runner
                    (var runner, int runner_number)= session.CreateSequenceRunner(sync_source, HttpContext);
                    //Make an external identifier
                    ExtRunnerKey key = (session, runner_number); 
                    //Pass the external identifier by a part of the redirection URL path.
                    //A value of an external identifier will be serialized by the Razor Pages framework using the key.ToString() method call
                    return RedirectToPage("SequenceShowResults", new { key }); 
            //...
        }

2.Значение внешнего идентификатора исполнителя из сегмента URL привязывается к входному параметру обработчика страницы с тем же именем. Привязка осуществляется с помощью класса привязки указанного в атрибуте [ModelBinder]. После получения внешнего идентификатора исполнителя обработчик проверяет, что исполнитель был создан в том же активном сеансе, в котором выполняется обработчик, и если это так — извлекает ссылку на исполнитель для дальнейшей работы с ним.


Pages\SequenceShowResults.cshtml


@page "{key}" 
@model SapmleApplication.Pages.SequenceShowResultsModel
<!-- ... -->

Pages\SequenceShowResults.cshtml.cs


    public class SequenceShowResultsModel : PageModel
    {
        //...
        public async Task OnGetAsync([ModelBinder<ExtRunnerKeyMvcModelBinder>]ExtRunnerKey Key)
        {
            //...
            IActiveSession active_session = HttpContext.GetActiveSession();
            if(!active_session.IsAvailable) {
                //... Write a message about the situation
            }
            else {
                if(!Key.IsForSession(active_session)) {
                    //... Write a message about the situation
                }
                else {
                    //Obtain the runner
                    var runner = active_session.GetSequenceRunner<SimSeqData>(Key.RunnerNumber, HttpContext);
                    //...
                }
            }
        }
        //...
    }

3.Метод BindModelAsync класса привязки (он в нынешний состав библиотеки ActiveSession не входит, а реализован в самом примере) преобразует строку со значением параметра маршрутизации в экземпляр класса ExtRunnerKey — внешнего идентификатора исполнителя, используя метод ExtRunnerKey.TryParse.


ExtRunnerKeyMvcModelBinder.cs


    public class ExtRunnerKeyMvcModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext Context)
        {
            String name = Context.ModelName;
            String? key_string = Context.ValueProvider.GetValue(name).FirstOrDefault();
            if(key_string != null) {
                ExtRunnerKey key;
                if(ExtRunnerKey.TryParse(key_string, out key)) {
                    Context.ModelState.SetModelValue(name, key, key_string);
                    Context.Result=ModelBindingResult.Success(key);
                }
            }
            return Task.CompletedTask;
        }
    }

Сохранение объекта в свойстве Properties активного сеанса и отслеживание завершения и очистки сеанса с целью своевременной очистки сохраненного объекта

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


RunnerRegistry.cs


    public class RunnerRegistry: IDisposable
    {
        //...
        public void Dispose()
        {
            //...
        }

    }

Sources\RunnerRegistryActiveSessionsExtensions.cs


    public static class RunnerRegistryActiveSessionsExtensions
    {
        public const string REGISTRY_NAME = "RunnerRegistry";

        public static RunnerRegistry GetRegistry(this IActiveSession ActiveSession)
        {
            Object? cached_result;
            RunnerRegistry? result = null;
            //First just try to get the existing registry (fast path)
            if(!ActiveSession.Properties.TryGetValue(REGISTRY_NAME, out cached_result)) { //Fast path isn't available
                lock(ActiveSession.Properties) { //Lock shareable resource - Properties dictionary
                    result = new RunnerRegistry(); //Create new registry to add it to Properties
                    if(ActiveSession.Properties.TryAdd(REGISTRY_NAME, result)) //Use "double check pattern" with second check within lock block
                        //Addition was successful
                        ActiveSession.CleanupCompletionTask.ContinueWith((_) => result.Dispose()); //Plan disposing the registry added after end of ActiveSession
                    else {
                        //Somebody added a registry already between checks
                        cached_result = ActiveSession.Properties[REGISTRY_NAME]; //Get previously added registry
                        result.Dispose();
                        result=null; //Dispose and clear result
                    }
                }
            }
            return result??(RunnerRegistry)cached_result!; //cached_result is not null here;
        }
        //...
    }


Библиотека ActiveSession и внедрение зависимостей



Получение сервисов с временем жизни Scoped для использования исполнителями


Как известно, при работе с ASP.NET Core широко используется внедрение зависимостей: автоматическая передача в конструктор (или другой метод) аргументов, получаемых из контейнера сервисов. Так вот, при внедрении зависимостей в обработчики веб-запросов необходимо соблюдать следующее правило: зависимости, которые будут переданы исполнителю библиотеки ActiveSession и которые зарегистрированы в контейнере сервисов с временем жизни ограниченной области(Scoped), следует запрашивать через специальный сервис-адаптер IActiveSessionService.


Почему?

Проблема возникает с теми сервисами, для которых при регистрации указано время существования, ограниченное областью действия (Scoped). В их число входит немалое количество часто используемых сервисов — например, представляющих подключения к базам данных или контексты баз данных Entity Framework. Стандартно в ASP.NET Core такой областью действия сервиса, ограничивающей его время жизни, является обработка одного запроса к веб-серверу. Такие сервисы исполнители использовать, очевидно, не должны, потому что, по самой их сути, часть кода исполнителей выполняется вне контекста обработки какого-либо запроса. Поэтому для активного сеанса создается своя область действия сервисов, существующая, пока существует активный сеанс.


Технически, для создания областей действия сервисов в .NET используются дочерние контейнеры сервисов: сервис с временем жизни области действия, полученный из дочернего контейнера для определенной области, имеет время жизни этой области. Для обычного обработчика запроса ASP.NET Core для области действия, связанной с запросом, ссылка на ее контейнер помещается в контекст запроса (свойство HttpContext.RequestServices). В библиотеке ActiveSession ссылка на контейнер сервисов области для активного сеанса хранится в свойстве SessionServices активного сеанса (IActiveSession.Service). Именно этот дочерний контейнер передается в метод Create фабрики исполнителя, создающий исполнитель.


Остается ещё одна проблема: внедрение зависимостей от сервисов с временем жизни ограниченной области, используемых в исполнителях, в обработчики запроса, реализуемые в рамках фреймворков высокого уровня (например — контроллеры и действия ASP.NET Core MVC или страницы Razor pages). Дело в том, что такие фреймворки получают объекты сервисов для области действия связанной с запросом, а про активный сеанс и ее область действия для сервисов им ничего неизвестно. В результате исполнитель может получить в качестве аргумента ссылку на сервис с неподходящей областью действия.


В библиотеке ActiveSession предусмотрены, в принципе, два варианта решения этой проблемы.


Первый вариант — подмена ссылки на дочерний контейнер сервисов в свойстве HttpContext.RequestServices контекста запроса на дочерний контейнер для области действия активного сеанса. Такая подмена настраивается через параметр UseSessionServicesAsRequestServices при конфигурировании библиотеки (в этой статье конфигурирование библиотеки ActiveSession не рассматривается). В этом случае все зависимости от сервисов с временем жизни ограниченной области разрешаются для области действия активного сеанса, что автоматически позволяет передавать любые из них исполнителям.
Достоинством этого варианта является отсутствие необходимости дополнительных модификаций уже написанного кода. Недостатками является, во-первых, чувствительность к порядку установки компонентов-обработчиков (middleware) в конвейер: может получиться нехорошо, если другой компонент-обработчик до подмены контейнера получит объект сервиса из контейнера, связанного с запросом, а обработчик конечной точки будет работать с совсем другим объектом, полученным из контейнера сервисов активного сеанса. Другой недостаток этого варианта — он, в общем случае, не тестировался для фреймворков во всем разнообразии их применений, а потому нет никакой гарантии, что подмена контейнера сервисов не вызовет ошибок во фреймворке. Потому что, например, для разных запросов будет возвращаться один и тот же экземпляр реализующего сервиса, тогда как логика работы фреймворка может быть рассчитана на то, что экземпляры для каждого запроса будут разными. А потому этот вариант не рекомендуется использовать без подробного тестирования приложения. В том числе — и нагрузочного, поскольку сервисы с областью действия активного сеанса могут жить дольше, а потому — требовать большего расхода памяти приложением. В общем, такой вариант в библиотеке ActiveSession реализуем, но не рекомендован.


Второй, более безопасный вариант — это внедрение зависимостей от сервисов, предназначенных для передачи исполнителям, через специальный сервис-адаптер IActiveSessionService<TService>. Этот сервис зарегистрирован, как имеющий время жизни ограниченной области (Scoped), и при внедрении зависимостей его реализация запрашивается, естественно из контейнера сервисов для обработки запроса. То есть, сам объект, реализующий IActiveSessionService, привязан к области обработки одного запроса к веб-серверу: он будет одним и тем же в этой области, и он будет очищен по завершении обработки этого веб-запроса. Но вот сервис, для которого IActiveSessionService служит адаптером, и ссылку на который содержит свойство IActiveSessionService.Service, запрашивается из контейнера сервисов активного сеанса (если она доступна), а потому по завершении обработки веб-запроса реализующий его экземпляр объекта очищен не будет, и получение этого же сервиса в другом запросе, входящем в тот же активный сеанс, вернет этот же экземпляр.
Достоинством этого варианта является его лучшая совместимость: изменение времени жизни сервисов касается только тех из них, для которых оно указано явно, никакие другие сервисы (в том числе, и вызываемые в других компонентах-обработчиках конвейера(middleware) это изменение не затронет. Потому рекомендуется именно этот вариант решения проблемы внедрения зависимостей для ограниченной области. Недостатки у этого варианта тоже есть. Во-первых, это — необходимость модификации существующего исходного кода (если таковой уже есть, конечно). А, во-вторых — несколько большие накладные расходы на поиск сервиса: для доступа к активного сеанса, в которой содержится ссылка на ее контейнер, требуется получить текущий контекст запроса (HttpContext), а в рамках работы контейнера сервисов это возможно только через сервис IHttpContextAccessor. Сервис же IHttpContextAccessor работает через доступ к контексту выполнения (переменной типа AsyncLocal), а это само по себе ведет к увеличению накладных расходов — не знаю, насколько больших во всех случаях, но, по крайней мере, достаточному, чтобы о нем, упоминалось в документации по .NET. Впрочем, полученный от IHttpAccessor контекст запроса кэшируется в экземпляре внутреннего сервиса библиотеки ActiveSession, имеющего время жизни области обработки запроса, а потому в течение обработки одного запроса обращаться к сервису IHttpContextAccessor приходится не более одного раза.


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


Использование сервиса-адаптера IActiveSessionService<TService> — это удобный способ получения в обработчике запроса экземпляра сервиса TService, который будет существовать необходимое для передачи его в исполнитель время, поскольку сервис при этом получается из контейнера сервисов активного сеанса. Но использовать этот адаптер следует только в случае, если такой сервис поддерживает параллельный доступ к нему. Если это условие не выполняется — см. следующий раздел.


Свойство Service интерфейса сервиса-адаптера IActiveSessionService<TService> содержит ссылку на объект, реализующий реально требующийся сервис TService. Если этот сервис не может быть получен либо потому, что сервис не зарегистрирован в контейнере, либо по какой-то другой причине, это свойство будет содержать null. Сам сервис-адаптер IActiveSessionService<TService> является одним из сервисов инфраструктуры библиотеки ActiveSession. Он регистрируется со временем жизни Scoped и предназначен для получения из контейнера сервисов запроса. Поэтому он пригоден для внедрения его в качестве зависимости в поддерживающих внедрение зависимости фреймворках.


Время жизни объекта, ссылку на который содержит свойство Service адаптера, определяется тем, доступен ли активный сеанс: если да, то полученный экземпляр сервиса будет существовать и оставаться одним и тем же в течение всего активного сеанса, и его можно передать в исполнитель. Если же активный сеанс окажется недоступным, то экземпляр объекта сервиса все равно будет получен, но — из контейнера сервисов запроса. И существовать он, в таком случае, будет только во время обработки этого запроса. В исполнитель его передавать нельзя — впрочем, раз активный сеанс недоступен, создать исполнитель или получить существующий все равно не удастся. Так сделано из соображений совместимости — чтобы нужный сервис был доступен обработчику единообразно вне зависимости от доступности активного сеанса. То есть — чтобы можно было в любом случае сначала получить через параметры конструктора класса, содержащего обработчик (в MVC или Razor Pages), или метода-обработчика (в Minimal API) с помощью механизма внедрения зависимости экземпляр сервиса-адаптера, содержащий ссылку на нужный сервис, а уже внутри обработчика разбираться, доступен ли активный сеанс, в котором можно было бы создать использующий этот сервис исполнитель. Использовать же этот полученный через адаптер сервис в рамках одного запроса можно в любом случае.


На то, откуда получен экземпляр сервиса, указывает свойство сервиса-адаптера IActiveSessionService.IsFromSession.



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


Некоторые сервисы, предназначенные для регистрации с временем жизни Scoped, не допускают для параллельный доступ к одному и тому же экземпляру. Важными примерами таких сервисов являются контексты базы данных из Entity Framework Core, зарегистрированные как сервисы. Такие сервисы хорошо подходят для использования традиционным процессом обработки HTTP-запросов, где каждый запрос имеет свою собственную область действия, поэтому экземпляр объекта, реализующего такой сервис для конкретного запроса, доступен только обработчику этого запроса. Но использование таких сервисов, если они получены из контейнера сервисов для области действия активного сеанса, а не контейнера сервиса запроса, создает проблему: единственный экземпляр сервиса потенциально может оказаться в совместном использовании несколькими обработчиками запросов, которые выполняются одновременно, если эти запросы принадлежат одному и тому же активному сеансу.


Для решения этой проблемы библиотека ActiveSession содержит функцию взаимоисключающего доступа к сервису. Объект взаимоисключающего доступа к сервису, представленный обобщенным интерфейсом ILockedSessionService<TService>, где TService — это тип сервиса, обеспечивает получение взаимоисключающего доступа к сервису указанного типа, полученному из контейнера сервисов активного сеанса. Объект взаимоисключающего доступа к сервису типа TService может быть получен вызовом асинхронного метода AcquireAsync обобщенного интерфейса ISessionServiceLock<TService>, специализированного типом нужного сервиса(TService). Пока один такой такой объект для сервиса указанного типа в рамках того же активного сеанса существует и не очищен, метод AcquireAsync не вернет следующий объект взаимоисключающего доступа, а будет ждать очистки предыдущего.


Ссылка на объект, реализующий сервис, содержится в свойстве Service объекта взаимоисключающего доступа ILockedSessionService<TService>, аналогично сервису-адаптеру для получения сервиса из контейнера сервисов активного сеанса, IActiveSessionService<TService>. Если сервис из контейнера получить не удалось (потому что он не зарегистрирован или по какой-то другой причине), эта ссылка будет содержать null .


Так же как и сервис-адаптер, IActiveSessionService<TService>, функция взаимоисключающего доступа к сервису может быть использована и в условиях, когда активный сеанс недоступен. В этом случае свойство Service объекта взаимоисключающего доступа будет содержать ссылку на объект, реализующий сервис, полученный из контейнера сервисов запроса, аналогично сервису-адаптеру. А так как доступ к этому экземпляру сервиса из обработчиков других запросов в этом случае невозможен, то метод ISessionServiceLock<TService>.AcquireAsync не будет ничего ожидать, а сразу вернет объект взаимоисключающего доступа к сервису. И свойство IsReallyLocked такого объекта будет содержать false, тогда как для сервиса, полученного из контейнера сервисов сеанса оно будет равно true. Такое решение делает объект взаимоисключающего доступа к сервису таким же универсальным, как и сервис-адаптер IActiveSessionService<TService>: его точно так же можно передавать через параметры путем внедрения зависимостей, а полученный через него сервис можно использовать в рамках обработчика одного запроса одинаковым образом вне зависимости от доступности активного сеанса.


Интерфейс ISessionServiceLock<TService> является одним из сервисов инфраструктуры библиотеки ActiveSession. Он регистрируется в контейнере сервисов приложения с временем жизни Scoped и предназначен для получения его из контейнера сервисов запроса. Поэтому его можно внедрить как зависимость в любой класс или метод, который использует фреймворк, поддерживающий внедрение зависимостей.


Библиотека ActiveSession предоставляет метод расширения CreateRunnerWithExclusiveService интерфейса IActiveSession для создания исполнителя, использующего сервис с взаимоисключающим доступом. Этот метод является аналогом IActiveSession.CreateRunner: он имеет же параметры плюс ещё один дополнительный параметр — объект взаимоисключающего доступа к сервису. Этот объект будет очищен автоматически после завершения и очистки исполнителя. Кроме того, все методы расширения для создания стандартных исполнителей также принимают необязательный параметр — объект взаимоисключающего доступа к сервису, который будет очищен после завершения и очистки соответствующего исполнителя, созданного этими методами.


Пример использования функции взаимоисключающего доступа в классе модели страницы Razor Pages

Pages\ExclusiveParams.cshtml.cs


    public class ExclusiveParamsModel : PageModel
    {
        // ...Other fields and properties
        //A reference to the service used to obtain an exclusive service accessor
        readonly ISessionServiceLock<IExclusiveService> _sessionServiceLock;
        // ...
        //Inject ISessionServiceLock service reference into the page model class constructor
        public ExclusiveParamsModel(ISessionServiceLock<IExclusiveService> SessionServiceLock)
        {
            // ...Initialize other fields and properties
            _sessionServiceLock=SessionServiceLock;
        }

        public async Task<ActionResult> OnPostAsync() 
        {
            IActiveSession session = HttpContext.GetActiveSession();
            // ...Perform some initialization and check if the session exists

            //An exclusive service accessor. The service represented by it will not be really used by this example however.
            ILockedSessionService<IExclusiveService>? accessor; 
            try {
                // Obtain an exclusive service accessor to lock access to the instance implementing IExclusiveService
                accessor =  await _sessionServiceLock.AcquireAsync(Timeout.InfiniteTimeSpan, HttpContext.RequestAborted);
            }
            catch(ObjectDisposedException) {
                //The active session was terminated, and it and all its associated objects are disposed.
                return RedirectToPage("SessionGone"); //Redirect to a page informing a user about that
            }
            if(accessor == null) return StatusCode(StatusCodes.Status500InternalServerError); //Infinite wait ended? It's impossible!

            IAsyncEnumerable<SimSeqData> async_source; //Its initialized is skipped for brevity.
            //Create a runner and pass the exclusive service accessor obtained earlier to be disposed after the runner completion and cleanup
            (IRunner runner, int runner_number) = session.CreateSequenceRunner(async_source, HttpContext, accessor);

            //... return RedirectResult to the result page containing an URL with the runner external identifier
            //... see "Passing in URL an external runner identifier serialized into a string" example.          
        }

    }


Работа с исполнителем


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


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



Понятия, описывающие работу исполнителя

Прежде чем перейти к описанию свойств и методов интерфейса исполнителя IRunner<TResult> (и его не зависящей от типа результата части IRunner) стоит рассмотреть несколько понятий, на которые опирается это описание.



Состояние исполнителя.


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


Статус исполнителя, значение перечислимого типа RunnerStatus, описывает стадию жизненного цикла исполнителя. Существование исполнителя начинается с его начальной стадии, на которой фоновая операция еще не запущена. Значение статуса для этой стадии всегда NotStarted.


Когда начинается фоновое выполнение, исполнитель переходит на стадию выполнения. Существует два значения статуса, которые соответствуют стадии выполнения. Значение Stalled указывает, что результат для последней точки выполнения, достигнутой фоновой операцией, уже возвращен, но фоновая операция все еще выполняется. Значение Progressed указывает, что результат для последней точки выполнения, достигнутой фоновой операцией, еще не возвращен, при этом фоновая операция может быть как уже завершена, так и ещё выполняться. Чтобы проверить, соответствует ли значение статуса стадии выполнения, можно использовать метод расширения IsRunning() для класса RunnerStatus.


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


Состояние исполнителя, дошедшего до стадии завершения, больше измениться не может, а сам исполнитель по достижении этой стадии удаляется из хранилища. При этом исполнитель становится недоступным через интерфейс активного сеанса IActiveSession и подвергается очистке, если его класс подразумевает таковую (реализует интерфейс IDisposable и/или IAsyncDisposable). Чтобы проверить, соответствует ли значение статуса исполнителя стадии завершения, можно использовать метод расширения IsFinal() для класса RunnerStatus.


Позиция исполнителя — это номер точки выполнения, для которой были возвращены результаты фонового выполнения. Когда позиция передается вместе с возвращаемым результатом, она указывает точку выполнения, для которой возвращается результат. Свойство Position исполнителя содержит номер последней точки выполнения, для которой был возвращен результат. Обычно (а для любого стандартного исполнителя библиотеки ActiveSession — всегда) свойство Position — это еще и максимальный номер точки выполнения, для которой когда-либо возвращался результат. Позиция исполнителя изменяется только на стадии его выполнения. Для стандартных исполнителей библиотеки ActiveSession позиция исполнителя на начальной стадии всегда равна нулю. На стадии завершения у стандартных исполнителей позиция зависит от причины завершения. Если завершение исполнителя произошло из-за завершения фонового процесса — нормального (статус — Completed) или по ошибке (статус — Failed) — позиция завершенного исполнителя будет равна номеру последней достигнутой фоновым исполнителем точки выполнения: пока данные для этой точки выполнения не возвращены методами получения результата, работа исполнителя не завершается (но его выполнение можно прервать, и тогда он завершится сразу). Если же исполнитель был прерван (статус — Aborted) позиция его может быть любой: прерывание исполнителя прерывает выполнение и его фонового процесса, и относящихся к его интерфейсной части методов получения данных.


Исключение как часть состояния исполнителя устанавливается только тогда, когда исполнитель переходит в стадию завершения со статусом Failed. В этом случае эта часть состояния содержит исключение, которое произошло во время фонового выполнения. Во всех других случаях исключение равно null. Исключение как часть состояния исполнителя доступна через свойство Exception его интерфейса.


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


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



Исполнители последовательностей.


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


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


Во-вторых, результат исполнителя последовательности имеет обобщенный тип IEnumerable<TItem>, где TItem — тип записи. Результат, возвращаемый методами получения результатов последовательного исполнителя, является частью исходной фоновой последовательности, содержащей записи, которые еще не были возвращены — от последней точки выполнения, для которой был возвращен результат, и до точки выполнения, указанной параметрами метода (см. обсуждение параметров методов получения результатов ниже).


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


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


Краткое резюме того, что написано выше в скрытом тексте.


  1. Состояние исполнителя (точнее, его интерфейсной части) включает в себя его статус исполнителя — он указывает на текущую стадию выполнения исполнителя и содержит некую дополнительную информацию — и позицию исполнителя — это номер достигнутой точки выполнения, при этом отслеживать позицию имеет смысл, в основном, на стадии выполнения. Для завершенного исполнителя, фоновая операция которого завершилась с ошибкой, эта ошибка тоже входит в состояние исполнителя.
  2. Существует одна важная разновидность исполнителей — исполнители последовательностей. Фоновая часть исполнителей этой разновидности получает последовательность значений одного типа, именуемых записями, а интерфейсная часть — возвращает эту последовательность по частям, в том же порядке без пропусков и повторений. Для исполнителей последовательностей номером точки выполнения всегда служит число записей этой последовательности — полученных, если речь идет о фоновой части, или возвращенных — если об интерфейсной.


Методы получения результатов.


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


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


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


Второй из этих методов, GetAvailable, возвращает (синхронно) уже полученный результат фонового процесса — либо все, что было получено, либо для указанной через его параметры точки выполнения, если эта точка уже пройдена фоновым процессом, выполнение которого после этого продолжилось. Обычно этот метод вызывается для получения дополнительных результатов после первого вызова GetRequiredAsync.


Для исполнителей последовательностей это означает, что метод GetAvailable возвращает только записи, уже извлеченные фоновым процессом, причем — не более, чем указанное в параметре Advance число записей (при значении параметра Advance по умолчанию — все записи).


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


Рассмотрим сигнатуры методов получения результата.


public ValueTask<RunnerResult<TResult>> GetRequiredAsync(
    Int32 Advance = DEFAULT_ADVANCE,
    CancellationToken Token = default,
    Int32 StartPosition =CURRENT_POSITION,
    String? TraceIdentifier=null
);

public RunnerResult<TResult> GetAvailable(
    Int32 Advance = MAXIMUM_ADVANCE, 
    Int32 StartPosition = CURRENT_POSITION, 
    String? TraceIdentifier = null
);

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


Параметры Advance и StartPosition определяют для каких именно номеров точек выполнения исполнителя должен быть возвращен результат. Значением параметра StartPosition, может быть либо число — номер начальной точки выполнения, либо константа CURRENT_POSITION=-1, означающая использование в качестве номера начальной точки значения текущей позиции исполнителя. Значение параметра Advance может быть либо число — максимальный размер диапазона номеров точек выполнения, для которого возвращается результат, либо константа DEFAULT_ADVANCE=0, интерпретация которой зависит от исполнителя.


Говоря формально, ...

… параметры Advance и StartPosition определяют диапазон номеров точек выполнения, для которого должен быть возвращен результат. А именно, параметр StartPosition указывает начало этого диапазона, а Advance — его размер, то есть, на какую величину номер конечной точки выполнения может превосходить номер начальной точки.
Но это — именно формальное определение.


Какой именно смысл эти параметры имеют для конкретных исполнителей, и какие на них наложены ограничения, определяется самим исполнителем, его типом.


В частности, для исполнителей последовательностей существуют следующие правила. Параметр StartPosition обязан быть или константой CURRENT_POSITION, или совпадать с текущей позицией исполнителя (значением его свойства Position). Параметр Advance — это максимальное число записей в возвращаемом результате, и значение этого параметра не может быть отрицательным.


Значения параметров Advance и StartPosition по умолчанию

Значение по умолчанию параметра Advance для методов получения результата отличается. Для GetAvailable оно равно Int32.MaxValue, то есть по умолчанию этот метод возвращает результат для последней достигнутой фоновым процессом точки выполнения (номер которой, очевидно, меньше Int32.MaxValue). То есть, для исполнителей последовательностей записей, результатом будут все уже выбранные в фоне, но ещё не возвращенные ранее записи.


Для GetRequiredAsync значение по умолчанию параметра Advance равно константе IRunner.DEFAULT_ADVANCE (=0). Для исполнителей последовательностей вместо него подставляется величина значения по умолчанию, указанная при создании исполнителя (поле DefaultAdvance объекта параметров исполнителя). Если при создании исполнителя значение этого поля не было задано, то подставляется значение, заданное в конфигурации библиотеки ActiveSession (которое, в свою очередь, по умолчанию равно 20).


Параметр StartPosition по умолчанию имеет значение IRunner.CURRENT_POSITION (-1), что означает, что в качестве начальной используется текущая позиция исполнителя на момент вызова метода (значение его свойства Position).


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


Параметр Token метода GetRequiredAsync служит для отмены выполнения вызова этого метода через стандартный для .NET механизм согласованной отмены. При этом отменяется только этот конкретный метод, выполнение фонового процесса при этом продолжается и его результат можно получить последующими вызовами GetAvalable или GetRequiredAsync. Следует отметить, что для исполнителей последовательностей при отмене вызова метода GetRequiredAsync (а равно и при возникновении исключения при его выполнении), никакие записи из полученной фоновым процессом последовательности не пропадают: они будут возвращены следующим вызовом GetAvalable или GetRequiredAsync.


Оба метода получения результата возвращают не только сам по себе результат, но и состояние исполнителя (подробности о нем — в скрытом тексте в начале главы) на момент получения результата в структуре типа RunnerResult<TResult>. Метод GetRequiredAsync, так как он выполняется асинхронно, возвращает задачу с результатом типа RunnerResult<TResult> (значение типа ValueTask<RunnerResult<TResult>> ), которую можно использовать для ожидания завершения и получения результата. Метод GetAvailable, поскольку он выполняется синхронно, просто возвращает структуру типа RunnerResult<TResult> .


С возвращаемой структурой удобнее всего работать с помощью присваивания с разборкой, например, так (для примера использован метод GetAvailable):
(TResult result, RunnerStatus status, Int32 position, Exception? exception)=runner.GetAvailable();



Получение информации о процессе фонового выполнения.


Метод GetProgress() интерфейса исполнителя возвращает информацию о выполнении фонового процесса. Он не имеет параметров и возвращает пару значений, объединенных в структуру RunnerBkgProgress: номер последней точки выполнения, достигнутой фоновым процессом, и оценку номера конечной точки выполнения фонового процесса, если эта оценка возможна (иначе — null). Значение, возвращаемое этим методом, удобнее всего обрабатывать с помощью присваивания с разборкой, например, так: (Int32 progress, Int32? end) = runner .GetProgress();


Свойство Boolean IsBackgroundExecutionCompleted интерфейса исполнителя указывает, завершен ли процесс фонового выполнения исполнителя.



Прерывание выполнения исполнителя.


Метод Abort служит для немедленного прекращения выполнения исполнителя. Он имеет единственный необязательный параметр TraceIdentifier, который используется для трассировки совершенно аналогично одноименному параметру методов получения результата. Этот метод возвращает статус исполнителя, соответствующий той причине, по которой исполнитель был реально завершен: он не обязательно равен Aborted, потому что исполнитель мог завершиться ранее по другой причине.



Отслеживание завершения работы и очистки исполнителя


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


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



Другие свойства исполнителя
  1. Идентификатор исполнителя Id. Тип свойства — это структура RunnerId. Она содержит два поля: String? ID — идентификатор активного сеанса, в которой работает исполнитель и Int32 RunnerNumber — номер исполнителя. Это свойство служит, в основном, для целей трассировки: его значение, если оно указано, добавляется кодом исполнителя и инфраструктуры как параметр каждой отправляемой в журнал (log) записи, связанной с этим исполнителем.
  2. Дополнительные данные: Object? ExtraData — произвольные дополнительные данные, сопоставленные с исполнителем. Ответственность за очистку этих дополнительных данных, если она необходима, лежит целиком на приложении. Для выполнения своевременной очистки приложение может связать функцию обратного вызова с маркером завершения либо дождаться завершения задачи очистки исполнителя, ссылку на которую можно получить методом CleanupCompletionTask интерфейса активного сеанса IActiveSession.


Примеры работы с исполнителями.


Использование методов GetRequiredAsync и GetProgress и свойства IsBackgroundExecutionCompleted property (Razor Pages)

Из примера работы с исполнителями-адаптерами последовательностей: получение начальной порции данных.


Pages\SequenceShowResults.cshtml.cs


    public class SequenceShowResultsModel : PageModel
    {
        //...skip irrelevant code
        internal List<SimSeqData> _results=new List<SimSeqData>();
        internal RunnerStatus _status;
        internal Int32 _position;
        internal Exception? _exception;
        internal String RUNNER_COMPLETED = "The runner is completed.";
        //...skip irrelevant code
        internal Int32 _bkgProgress;
        internal Boolean _bkgIsCompleted;
        //...skip irrelevant code
        public String StartupStatusMessage { get; private set; } = "";

        public async Task OnGetAsync([ModelBinder<ExtRunnerKeyMvcModelBinder>]ExtRunnerKey Key)
        {
            //...skip irrelevant code and adjust indentation
            var runner = active_session.GetSequenceRunner<SimSeqData>(Key.RunnerNumber, HttpContext);
            //...skip irrelevant code and adjust indentation
            IEnumerable<SimSeqData> res_enum;
            (res_enum,_status,_position,_exception) = 
                await runner.GetRequiredAsync(_params?.StartCount??IRunner.DEFAULT_ADVANCE, TraceIdentifier: HttpContext.TraceIdentifier);
            _results = res_enum.ToList();
            if(_status.IsFinal()) StartupStatusMessage=RUNNER_COMPLETED;
            else {
                StartupStatusMessage="The runner is running in background.";
            }
            _bkgIsCompleted = runner.IsBackgroundExecutionCompleted;
            _bkgProgress = runner.GetProgress().Progress;
            //...skip irrelevant code and adjust indentation
        }

Pages\SequenceShowResults.cshtml


@page "{key}" 
@model SapmleApplication.Pages.SequenceShowResultsModel
<!-- ... --->
<div style="display:inline-block; min-width:30%">
    <h3 style="text-align:center">Example results.</h3>
    <div>
        <span style="margin-left: 10px; font-weight:600">Status:</span> <span id="runner_status">@Model._status</span>
        <span style="margin-left: 10px; font-weight:600">#Records:</span> <span id="position">@Model._position</span>
    </div>
    <div>
        <span style="margin-left: 10px; font-weight:600">Bkg. progress:</span> <span id="bkg_progress">@Model._bkgProgress</span>
        <span style="margin-left: 10px; font-weight:600">Bkg. is completed:</span> <span id="bkg_completed">@Model._bkgIsCompleted</span>
    </div>
    <div class="records number">Number</div><div class="records name">Name</div><div class="records name">Data</div>
    <table style="display:block;border-style:solid;border-width:thin;height:15em;overflow:auto">
        <tbody id="results_table">
            @for(int row = 0; row<Model._position; row++) {
                <tr>
                    <td class="records number">@(Model._results[row].Number+1)</td>
                    <td class="records name">@Model._results[row].Name</td>
                    <td class="records data">@Model._results[row].Data</td>
                </tr>
            }
        </tbody>
    </table>
    <!-- ... --->
</div>
<!-- ... --->

Использование методов GetAvailable и GetProgress и свойства IsBackgroundExecutionCompleted property (MVC API controller)

Из примера работы с исполнителями-адаптерами последовательностей: получение дополнительных данных.
APIControllers\SampleController.cs


    //public class GetAvailableRequest
    //{
    //    public ExtRunnerKey RunnerKey { get; set; }
    //    public Int32? Advance { get; set; }
    //}

    //public class SampleSequenceResponse
    //{
    //    public Int32 status { get; init; } = StatusCodes.Status200OK;
    //    public String? runnerStatus { get; set; }
    //    public Int32 position { get; set; }
    //    public Exception? exception { get; set; }
    //    public IEnumerable<SimSeqData>? result { get; set; }
    //    public Boolean isBackgroundExecutionCompleted { get; set; }
    //    public Int32 backgroundProgress { get; set; }
    //}

    [Route("api")]
    [ApiController]
    public class SampleController : ControllerBase
    {
        [HttpPost("[action]")]
        public ActionResult<SampleSequenceResponse> GetAvailable(GetAvailableRequest Request)
        {
            //...skip irrelevant code and adjust indentation
            // IEnumerable<SimSeqData> runner; Initialized elsewhere
            SampleSequenceResponse response = new SampleSequenceResponse();
            response.backgroundProgress=runner.GetProgress().Progress;
            response.isBackgroundExecutionCompleted=runner.IsBackgroundExecutionCompleted;
            RunnerStatus runner_status;
            (response.result, runner_status, response.position, response.exception) =
                runner.GetAvailable(Request.Advance??Int32.MaxValue, TraceIdentifier: HttpContext.TraceIdentifier);
            response.runnerStatus=runner_status.ToString();
            return response;
            //...skip irrelevant code and adjust indentation
        }
        //...skip irrelevant code and adjust indentation
    }

Прерывание выполнения исполнителя методом Abort через типонезависимую часть интерфейса (MVC API controller)

Из примеров работы с разными исполнителями: реакция на закрытие страницы в браузере.


APIControllers\SampleController.cs


    //public class AbortRequest
    //{
    //        public ExtRunnerKey RunnerKey { get; set; }
    //}
    //
    //public class AbortResponse
    //{
    //    public Int32 status { get; init; } = StatusCodes.Status200OK;
    //    public String? runnerStatus { get; set; }
    //}

    public ActionResult<AbortResponse> Abort(AbortRequest Request)
    {
        IActiveSession session = HttpContext.GetActiveSession();
        if(session.IsAvailable && Request.RunnerKey.IsForSession(session)) {
            IRunner runner = session.GetNonTypedRunner(Request.RunnerKey.RunnerNumber, HttpContext);
            if(runner!=null) {
                AbortResponse response = new AbortResponse();
                response.runnerStatus=runner.Abort(HttpContext.TraceIdentifier).ToString();
                return response;
            }
        }
        return StatusCode(StatusCodes.Status410Gone);
    }

Использование свойства ExtraData исполнителя для передачи связанной с ним информации между обработчиками запросов (Razor Pages)

Из примера работы с исполнителями-адаптерами последовательностей: переход на страницу отображения результатов.


Pages\SequenceAdapterParams.cshtml.cs


        public ActionResult OnPost() 
        {
            //...skip irrelevant code and adjust indentation
            SequenceParams seq_params=MakeSequenceParams();
            //...skip irrelevant code and adjust indentation
            IEnumerable<SimSeqData> sync_source = new SyncDelayedEnumerble<SimSeqData>(seq_params.Stages, new SimSeqDataProducer().Sample);
            (IRunner runner, int runner_number)= session.CreateSequenceRunner(sync_source, HttpContext);
            //...skip irrelevant code and adjust indentation
            ExtRunnerKey key = (session, runner_number);
            runner.ExtraData=seq_params;
            return RedirectToPage("SequenceShowResults", new { key });
            //...skip irrelevant code and adjust indentation
        }

Pages\SequenceShowResults.cshtml.cs


    public class SequenceShowResultsModel : PageModel
    {
        internal SequenceParams? _params;
        //...skip irrelevant code
        public async Task OnGetAsync([ModelBinder<ExtRunnerKeyMvcModelBinder>]ExtRunnerKey Key)
        {
            //...skip irrelevant code and adjust indentation
            var runner = active_session.GetSequenceRunner<SimSeqData>(Key.RunnerNumber, HttpContext);
            //...skip irrelevant code and adjust indentation
            _params = runner.ExtraData as SequenceParams;
            //...skip irrelevant code and adjust indentation
        }

Pages\SequenceShowResults.cshtml


@page "{key}" 
@model SapmleApplication.Pages.SequenceShowResultsModel
<!-- ... --->
<div style="display:inline-block; min-width:30%">
    <h3>Example parameters.</h3>
    <div style ="background-color: lightgray;">
        <div style="margin-bottom:0.2em"><b>Mode</b>:@(Model._params.Mode)</div>
        <div style="margin-bottom:0.2em"><b>Max #records at start</b>:@(Model._params.StartCount)</div>
        <div style="margin-bottom:0.2em"><b>Poll interval</b>:@(Model._params.PollInterval.TotalSeconds)s</div>
        <div style="margin-bottom:0.2em"><b>Max #records per poll</b>:
            @(Model._params.PollMaxCount.HasValue ? Model._params.PollMaxCount.Value.ToString():"not set")</div>
        <div>
            <div><b>Stages</b>:</div>
            <table style="margin-left:5px">
                <tr><th>#</th><th>Iterations:</th><th>Delay:</th><th>Ends at:</th></tr>
                @if (Model._params.Stages != null) {
                    int num = 0;
                    int end_pos = 0;
                    @foreach (SimStage stage in Model._params.Stages) {
                        <tr><td style="min-width:2em">@(num++)</td><td>@stage.Count</td><td>@(Model.SmartInterval(stage.Delay))</td><td>@(end_pos+=stage.Count)</td></tr>
                    }
                }
            </table>
        </div>
    </div>
</div>
<!-- ... --->

Отслеживание завершения исполнителя с использованием его маркера завершения IRunner.CompletionToken

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


Sources\RunnerRegistryObserver.cs


        public async Task Observe(Action<Int32, Int32?> Callback, CancellationToken CompletionToken)
        {
            TaskCompletionSource<Int32> wait_source;
            TaskCompletionSource<Int32> completion_source =new TaskCompletionSource<Int32>();
            Callback.Invoke(_registry.Count, null);
            using(
                CancellationTokenRegistration completion_registration= CompletionToken.Register(
                    ()=>completion_source.SetCanceled(CompletionToken))
            ) {
                while(true) {
                    wait_source = Volatile.Read(in _currentWaitSource);
                    Int32 count = (await Task.WhenAny(wait_source.Task,completion_source.Task)).Result;
                    //One can come here only if wait_source.Task is ran to completion,
                    // because completion_source.Task never runs to completion, it can be completed via an OperationCanceledException only.
                    Callback(count, null);
                }
            }
            //One never come here to run this task to completion, as a loop above can be be terminated by a OperationCanceledException
            //This exception will be intercepted by calling code as an expected one.
        }

    }

Отслеживание очистки завершенного исполнителя с использованием метода IActiveSession.TrackRunnerCleanup

Реализация метод расширения CreateRunnerWithExclusiveService для интерфейса IActiveSession (из исходного кода библиотеки ActiveSession).


        public static KeyedRunner<TResult> CreateRunnerWithExclusiveService<TRequest,TResult> (
            this IActiveSession Session,
            TRequest Request,
            HttpContext Context,
            IDisposable ExclusiveServiceAccessor)
        {
            return InternalCreateRunnerExcl<TRequest,TResult>(Session, Request, Context, ExclusiveServiceAccessor);
        }

        internal static KeyedRunner<TResult> InternalCreateRunnerExcl<TRequest, TResult>(
            IActiveSession Session,
            TRequest Request,
            HttpContext Context,
            IDisposable? ExclusiveServiceAccessor)
        {
            KeyedRunner<TResult> result = Session.CreateRunner<TRequest, TResult>(Request, Context);
            if(ExclusiveServiceAccessor!=null) {
                (Session.TrackRunnerCleanup(result.RunnerNumber)??Task.CompletedTask)
                    .ContinueWith((_) => ExclusiveServiceAccessor.Dispose(),TaskContinuationOptions.ExecuteSynchronously);
            }
            return result;
        }


Классы стандартных исполнителей библиотеки ActiveSession


В настоящее время библиотека ActiveSession реализует четыре стандартных класса исполнителей. Три из них — универсальные классы EnumAdapterRunner, AsyncEnumAdapterRunner и TimeSeriesRunner — являются классами исполнителей последовательностей. Общие для таких исполнителей черты были рассмотрены ранее (частично, в скрытом тексте).

Примеры использования стандартных исполнителей демонстрируются в проекте SampleApplication в репозитории ActiveSessionExamples.



Классы-адаптеры для внешних последовательностей.


Первые два обобщенных класса исполнителей последовательностей, перечисленные выше, EnumAdapterRunner<TItem> и AsyncEnumAdapterRunner<TItem>, очень похожи, и потому их разумно обсуждать вместе. Оба этих класса получают входные последовательности (или перечисления) элементов (записей) типа TItem через свои конструкторы. Затем они перечисляют эти последовательности в фоновом режиме, возвращая части этих последовательностей через вызовы методов результата GetAvailable и GetRequiredAsync, как описано ранее.


Разница между этими двумя классами заключается в том, как они перечисляют входные последовательности. Класс EnumAdapterRunner<TItem> получает входную последовательность типа IEnumreable<TItem>, которая может быть перечислена только синхронно. Любое ожидание в процессе перечисления блокирует поток, который выполняет перечисление. При этом предполагается, что скорость перечисления ограничивается вводом/выводом, поэтому перечисление производится в фоновом режиме в отдельной задаче, исполнять которую планировщику указано, по возможности, в своем отдельном потоке. Класс AsyncEnumAdapterRunner<TItem> получает в качестве источника асинхронно перечислимую последовательность типа IAsyncEnumreable<TItem>, перечисление которой не приводит к блокировке потока.


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


Описание структур с дополнительными параметрами

Упомянутые структуры с дополнительными параметрами имеют обобщенные типы EnumAdapterParams<TItem> и AsyncEnumAdapterParams<TItem> соответственно. Эти структуры для обоих классов адаптеров очень похожи: они содержат, кроме поля Source с перечисляемой последовательностью, тип которой отличается, набор одинаковых полей. Некоторые из этих полей перечислены ниже:


  • Поле int? DefaultAdvance — содержит значение по умолчанию для параметра Advance метода GetRequiredAsync (см. описание этого параметра в главе про исполнители).
  • Поле int? EnumAheadLimit — содержит предельное число выбранных в фоне но ещё не возвращенных записей, по достижении этого предела дальнейшая выборка блокируется, пока имеющиеся записи не будут переданы как часть результата метода, возвращающего результаты, по умолчанию это значение задается через конфигурирование библиотеки (в статье это не рассматривается).
  • Поле bool StartInCоnstructor — указывает, начнется ли перечисление сразу или при первом вызове метода GetRequiredAsync (последнее — вариант по умолчанию).
  • Поле bool PassSourceOnership — указывает, отвечает ли объект исполнителя при своей очистке (вызове его методов Dispose или DisposeAsync) за очистку объекта, реализующего входную последовательность, если тот реализует интерфейсы для очистки: IAsyncDisposable или IDisposable.

Для упрощения создания исполнителей указанных классов в библиотеке ActiveSession определены несколько перегруженных обобщенных методов расширения для интерфейса IActiveSession. Эти методы называются CreateSequenceRunner<TItem>. Их единственный параметр-тип TItem — тот же, что и параметр-тип перечисления, переданного через параметры. Набор параметров для каждого такого метода содержит, кроме обязательного для метода расширения первого параметра this IActiveSession и третьего параметра HttpContext Context (см. описание метода IActiveSession.CreateRunner), один из описанных выше входных параметров. Эти методы возвращают ту же структуру (KeyedRunner<IEnumerable<TItem>>), что и метод CreateRunner с соответствующими параметрами-типами. Тип создаваемого этими методами исполнителя — EnumAdapterParams<TItem> или AsyncEnumAdapterParams<TItem> — выбирается в соответствии с типом параметров конкретного метода.


Адаптеры последовательностей можно создать и непосредственно методом IActiveSession.CreateRunner

Для этого первым параметром в этот метод должен быть одним из входных параметров, описанных выше, первый параметр тип должен совпадать с типом этого параметра, а второй параметр тип (тип результата) должен быть IEnumerable<TItem>


Для упрощения поиска существующих исполнителей-адаптеров внешних последовательностей в библиотеке ActiveSession определен метод расширения GetSequenceRunner<TItem>, параметр-тип TItem которого определяет тип записей в последовательности (т.е. тип результата найденного исполнителя будет IEnumerable<TItem>).



Класс исполнителя, создающего временные ряды.


Класс TimeSeriesRunner<TResult> при своем фоновом выполнении создает временной ряд: последовательность пар ("момент измерения", "значение"). Значение имеет тип TResult, т.е. записи получаемой последовательности имеют тип (DateTime,TResult) (иначе говоря, ValueTuple<DateTime,TResult>). Значение для пары получается вызовом в момент измерения делегата-функции без параметров, которая возвращает значение типа TResult. Делегат (или содержащая его структура с параметрами, см. ее описание ниже) передается как первый аргумент при создании исполнителя. Измерения начинаются с момента первого вызова метода GetRequiredAsync (или с момента создания, если в метод создания передан через структуру с параметрами параметр StartInCоnstructor=true) и производятся с указанным как второй аргумент (или соответствующее поле в структуре с параметрами) интервалом времени. Ожидание истечения очередного интервала происходит асинхронно. Измерения производятся число раз, указанное в третьем, необязательном, аргументе. Если этот аргумент отсутствует, измерения производятся до прекращения выполнения исполнителя вызовом метода Abort из приложения или инфраструктурой библиотеки ActiveSession по истечении таймаута обращения к исполнителю, либо по факту завершения активного сеанса, в котором работает исполнитель.


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


Описание структуры с параметрами

Структура с параметрами имеет тип TimeSeriesparams<TResult>. Эта структура содержит упомянутые выше аргументы в полях, соответственно, Gauge, Interval и Count (если третий аргумент отсутствует, это поле устанавливается в null), а также — ряд дополнительных полей, аналогичных одноименным полям рассмотренных ранее структур (Async)EnumAdapterParams. Некоторые из этих полей перечислены ниже:


  • Поле int? DefaultAdvance содержит значение по умолчанию для параметра Advance метода GetRequiredAsync (см. описание этого параметра в главе про исполнители).
  • Поле int? EnumAheadLimit содержит предельное число выбранных в фоне но ещё не возвращенных записей, по достижении этого предела дальнейшая выборка блокируется, пока имеющиеся записи не будут переданы как часть результата метода, возвращающего результаты, по умолчанию это значение задается через конфигурирование.
  • Поле bool StartInCоnstructor указывает, начнется ли перечисление сразу или при первом вызове метода GetRequiredAsync (последнее — вариант по умолчанию).

Но для создания можно использовать напрямую и метод CreateRunner

Для этого первым параметром метода CreateRunner должна быть либо описанная выше структура с параметрами, либо двойка или тройка из описанных выше аргументов.
Тип первый параметра-тип при этом должен совпадать с типом переданного параметра, а второй параметр-тип должен быть IEnumerable<(DateTime, TResult)>


Для упрощения поиска существующих исполнителей, создающих временные ряды, в библиотеке ActiveSession определен метод расширения GetTimeSeriesRunner<TResult>, параметр-тип TResult которого определяет тип значений в парах время-значение, являющихся записями возвращаемой последовательности (т.е. тип результата найденного исполнителя будет IEnumerable<(DateTime, TResult)>).



Класс исполнителя сеансового процесса.


Исполнитель сеансового процесса (его тип — обобщенный класс SessionProcessRunner<TResult>) не относится к исполнителям последовательностей, а потому работает по несколько другим правилам. Его методы получения результата возвращают в качестве результата одиночные значения типа TResult. У него другие ограничения на параметры: ограничения на значение параметра Advance у него такие же — он должен быть больше или равен 0, но значение StartPosition может быть либо любой положительной величиной, не меньшей, чем текущее значение свойства Position, либо равно константе CURRENT_POSITION, т.е. -1, в последнем случае этот параметр заменяется на текущее значение свойства Position). Особая интерпретация для параметров у этого исполнителя одна: если параметры Advance и StartPosition имеют значения по умолчанию, принятые для метода GetRequiredAsync — StartPosition==CURRENT_POSITITION и Advance==0 — то Advance заменяется на 1, то есть, предполагается, что вызов с параметрами по умолчанию запрашивает получение результата для следующей точки выполнения. Сумма Advance и StartPosition (с учетом приведенных выше интерпретаций параметров) является номером точки выполнения, для которой запрашивается результат. И последнее отличие исполнителя сеансового процесса от исполнителей последовательностей — в том, что его методы получения результата можно вызывать независимо друг от друга: в любой момент для этого исполнителя может быть вызвано несколько методов GetRequiredAsync, асинхронно ожидающих достижения нужной точки выполнения, и при этом всегда можно вызвать синхронный метод GetAvailable.


Исполнитель сеансового процесса запускает (сразу, при своем создании) в качестве фонового процесса созданную им задачу. О том, как создается эта задача см. описание делегатов — параметров методов создания исполнителей этого типа ниже. Методу, создающему задачу фонового процесса передается функция-делегат обратного вызова и маркер отмены, который содержится в свойстве CompletionToken исполнителя. Этот маркер отмены может использоваться для согласованной отмены выполнения фонового процесса по завершении исполнителя.


Фоновый процесс вызывает функцию-делегат обратного вызова в выбранные им моменты времени. Эти вызовы и являются точками выполнения исполнителя сеансового процесса, а число вызовов функции обратного вызова является номером точки выполнения исполнителя сеансового процесса. Через функцию обратного вызова фоновый процесс передает промежуточный результат (типа TResult) и оценку номера завершающей точки выполнения (именно она возвращается методом GetProgress исполнителя). Эта оценка при разных вызовах функции обратного вызова может быть разной. Если такую оценку фоновый процесс произвести не может, то в качестве оценки передается null. Функция обратного вызова проверяет маркер отмены, который содержится в свойстве CompletionToken исполнителя, и, если отмена запрошена, вызывает исключение OperationCanceledException.


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


Завершении фонового процесса, вне зависимости от причины, завершает ожидание всех выполняющихся асинхронных вызовов GetRequiredAsync.


Результат и состояние возвращаемые при завершении

Выполняющиеся на момент завершения исполнителя вызовы GetRequiredAsync возвращают окончательный результат и состояние, соответствующее наступившей стадии завершения исполнителя: с соответствующим причине завершения статусом, позицией, равной номеру последней достигнутой точке выполнения, и исключением, если завершение вызвано исключением. Кроме того, оценка номера завершающей точки выполнения, возвращаемая методом GetProgress, становится при завершении фонового процесса равной номеру последней фактически достигнутой точки выполнения.


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


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


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


Во-первых, для создания исполнителя сеансового процесса могут использоваться либо делегаты, непосредственно создающие задачу фонового процесса (в частности, делегаты методов, помеченных как async), либо делегаты обычных (синхронных) методов, используемые как тело для создания задачи фонового процесса, выполняемой в одном из потоков пула — то есть, как параметры конструктора экземпляра соответствующего типа задачи: Task или Task<TResult>.


Во-вторых, создаваемые на основе делегатов задачи могут как возвращать результат (создаваемая с их помощью задача имеет тип Task<TResult>), так и не возвращать (создаваемая задача будет иметь тип Task). Для этого делегаты, непосредственно создающие задачу, возвращают задачу нужного типа, а делегаты тела задачи могут либо возвращать TResult, либо вообще не возвращать никакого результата.



Заключение


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

Комментарии (0)