Ну вот, я наконец-то сделал ее — первую версию библиотеки ActiveSession для ASP.NET Core. И для того, чтобы описать эту библиотеку, я написал эту статью.
Библиотека ActiveSession дает возможность, пока пользователь работает с веб-приложением в браузере, выполнять код на сервере в фоновом режиме. А результаты работы этого кода — и промежуточные, и окончательные — веб-приложение может потом получать через дополнительные запросы к серверу. Причем, этот код обрабатывает данные и выполняет операции, специфичные именно для этого конкретного экземпляра веб-приложения и работающего с ним конкретного пользователя: для разных пользователей, работающих с разными экземплярами, независимо выполняются разные, не связанные друг с другом экземпляры фоновой программы.
Если вам нужны (или просто интересны) такие возможности — читайте эту статью дальше.
Поскольку необъятное объять нельзя, то эта статья вынужденно ограничена описанием нескольких тем:
- Общее описание библиотеки — что она делает, из каких элементов она состоит, и какие элементы самого приложения необходимы для ее использования;
- Необходимый минимум действий, чтобы подключить библиотеку к приложению;
- Базовые сведения о том, как использовать библиотеку в обработчиках запросов HTTP;
- И немного общих слов не относящихся к тому, как использовать библиотеку: о том, почему и зачем я решил написать эту библиотеку, о ее лицензии, об ограничениях существующей версии и перспективах развития библиотеки (как я их вижу сейчас, естественно, ибо под влиянием различных обстоятельств все может поменяться).
Часть информации — полезной, но не являющейся совершенно необходимой для понимания — дана как скрытый текст, который при первом прочтении можно смело пропускать.
Картинка в начале статьи, вообще-то, служит для привлечения внимания читателей. Она так и называется: "картинка для привлечения внимания", сокращённо — КДПВ.
Впрочем, а данной статье я отступил от традиции брать в качестве КДПВ какой-нибудь переделанный мем или нечто абстрактное, созданное нейросетью, а использовал иллюстрацию по теме статьи. В данном случае на картинке схематически изображен один из сценариев выполнения кода в фоновом режиме. А рассказать об этом подробнее можно только с использованием несколько новых понятий, изложенных в статье. То есть — прочитать значительную часть статьи. Поэтому подробное объяснение помещено ближе к концу статьи.
Общее описание библиотеки ActiveSession
Начало работы с библиотекой ActiveSession
- Подключение библиотеки ActiveSession к приложению
- Выбор классов для исполнителей
- Конфигурирование сервисов
- Добавление в конвейер веб-приложения обработчика инфраструктуры библиотеки ActiveSession
Использование библиотеки ActiveSession в обработчиках запросов к веб-серверу.
Введение
Сразу, чтобы они были на виду, размещу ссылки на материалы по библиотеке ActiveSession:
- Репозиторий с исходным кодом библиотеки на GitHub.
- Сама библиотека в собранном виде оформлена как пакет NuGet с именем MVVrus.AspNetCore.ActiveSession. Этот пакет доступен на nuget.org.
- Документация по API библиотеки опубликована на GitHub Pages.
- Репозиторий с примерами использования библиотеки на GitHub.
Примеры в скрытом тексте, для которых указаны имена файлов, откуда взяты примеры (жирным шрифтом), содержатся в проекте SampleApplication в репозитории с примерами использования библиотеки (ссылка — выше). Имена файлов указаны относительно корневой папки этого проекта.
Общее описание библиотеки ActiveSession
Библиотека ActiveSession предназначена для использования в программах на базе фреймворка ASP.NET Core 6.0 и выше.
Для чего предназначена эта библиотека
Библиотека ActiveSession предназначена для выполнения операции в фоновом режиме и предоставления ее результатов клиенту через ответы на несколько логически связанных HTTP-запросов. Этот код, выполняющий фоновую операцию, используемые этим кодом общие данные и набор логически связанных запросов, возвращающих результаты операции будут далее называться активным сеансом (active session) или просто сеансом.
Фоновая операция инициируется обработчиком запроса, связанного с активным сеансом. Обычно, но не обязательно, этот, инициирующий, запрос является первым в сеансе. Он возвращает запрошенную через его параметры часть (иначе — диапазон) от полного результата операции — начальный результат, который передается в ответе на этот запрос клиенту. Обычно результат, возвращаемый инициирующим запросом, является промежуточным, и после того, как обработка этого, инициирующего запроса завершается, операция продолжается в фоновом режиме. Но если весь результат целиком попадает в запрошенный при получении начального результата диапазон, то в ответе на инициирующий запрос возвращается окончательный результат, и операция в фоновом режиме продолжаться не будет. Последующие запросы в сеансе, если они есть, возвращают в ответе клиенту результат, полученный в фоновом режиме между запросами — промежуточный или, если операция завершилась, окончательный. Кроме того, любой последующий запрос в сеансе может прервать фоновую операцию. Выполнение фоновой операции также прерывается библиотекой по истечении времени ожидания последующих запросов от клиента (по таймауту).
В отличие от другого механизма фонового выполнения в приложении ASP.NET Core, фоновых служб(Background Services), который является глобальным для всего приложения, каждый активный сеанс связан с одним клиентом, то есть он — разный для разных клиентов. Привязка активного сеанса к клиенту основана на функции сеансов (Sessions) ASP.NET Core: каждый активный сеанс связан с определенным сеансом, поддерживаемым этим механизмом. Активный сеанс становится доступным при обработке запроса, если запрос сделан в рамках соответствующего сеанса ASP.NET Core. При этом с одним сеансом ASP.NET Core, в принципе (например, в разное время), могут быть связаны несколько активных сеансов библиотеки ActiveSession. Но обработка каждого конкретного запроса всегда производится только в рамках одного активного сеанса, и обработчику запроса доступен только этот сеанс.
Компоненты библиотеки ActiveSession и ее расширений
Исполнители
Исполнители(runners) — это экземпляры классов, содержащие код операции, которая может выполняться в фоновом режиме. Для взаимодействия с приложением и другими частями библиотеки исполнители должны реализовывать интерфейс исполнителя.
То есть, для выполнения своих функций в приложении и для взаимодействия с остальными компонентами (инфраструктурой) библиотеки ActiveSession исполнитель должен быть оформлен в виде класса, реализующего интерфейс IRunner<TResult>
. Это — обобщенный интерфейс, его единственный параметр-тип (TResult) — это тип результата операции, производимой исполнителем. Часть свойств и методов интерфейса исполнителя от типа результата не зависит. Эти свойства и методы вынесены в отдельный, типонезависимый (иначе говоря, не обобщенный) интерфейс исполнителя — IRunner. Технически упомянутый выше обобщенный (полный) интерфейс исполнителя IRunner<TResult>
является наследником типонезависимого интерфейса исполнителя IRunner, а потому класс исполнителя, обязанный реализовывать полный интерфейс исполнителя автоматически реализует и типонезависимый интерфейс.
Каждый исполнитель целиком отвечает за выполнение своей операции:
- запуск операции;
- передачу начального результата выполнения запущенной операции и текущего состояния исполнителя в обработчик HTTP-запроса, запустивший операцию;
- выполнение операции в фоновом режиме;
- передачу информации о состоянии операции и полученного в фоновом режиме результата в обработчики последующих запросов того же сеанса;
- завершение операции — естественное, по ее выполнении или принудительное, вызванное либо обработчиком запроса, принадлежащего сеансу, в котором выполняется операция, либо по истечении времени ожидания следующего запроса.
Исполнители существуют всегда в рамках определенного активного сеанса. Каждому исполнителю в активном сеансе присваивается уникальный в рамках этого сеанса номер. По номеру исполнителя можно через интерфейс активного сеанса получить ссылку на этот исполнитель.
Для типовых сценариев использования в библиотеке ActiveSession в ней реализовано несколько классов стандартных исполнителей. Эти классы служат своего рода переходниками: они позволяют использовать в исполнителях код, который сам по себе не использует библиотеку ActiveSession.
Подробнее классы стандартных исполнителей рассмотрены в посвященной им дополнительной статье, а здесь я только перечислю их, с краткими пояснениями:
-
EnumAdapterRunner<TItem>
— создает исполнитель с типом результатаIEnumerable<TItem>
, возвращающий результаты от выполняющегося в фоне процесса, возвращающего последовательность записей типа TItem также с помощью интерфейсаIEnumerable<TItem>
; -
AsyncEnumAdapterRunner<TItem>
— аналогично предыдущему, но источником записей служит интерфейсIAsyncEnumerable<TItem>
; -
TimeSeriesRunner<TResult>
— возвращает последовательность пар типа (DateTime,TResult), получаемых вызовом заданной через входной параметр функции, вызываемой в моменты времени с заданным интервалом, т.е. имеет тип результатаIEnumerable<ValueTuple<DateTime,TResult>>
; -
SessionProcessRunner<TResult>
— запускает в фоновом режиме процесс, который возвращает через функцию обратного вызова промежуточные результаты своего выполнения.
Помимо стандартных исполнителей, библиотека ActiveSession поддерживает создание разработчиками своих собственных классов исполнителей.
Для облегчения реализации класса исполнителя в библиотеке ActiveSession есть предназначенный для этого класс: RunnerBase.Этот класс реализует базовую логику взаимодействия исполнителя с остальными элементами библиотеки и имеет защищенные методы, облегчающие реализацию специфической для приложения части исполнителя. Для облегчения реализации важной разновидности исполнителей — исполнителей последовательности (они описаны в главе про исполнители дополнительной статьи) в библиотеке реализован другой базовый класс: EnumerableRunnerBase<TItem>
.
Объекты активных сеансов
Объекты активных сеансов представляют собой существующие в текущий момент активные сеансы. Обработчик HTTP-запроса получает ссылку на объект активного сеанса, частью которого он является. Объект активного сеанса реализует интерфейс активного сеанса IActiveSession, с помощью которого обработчик запроса может взаимодействовать с библиотекой ActiveSession. В частности, обработчик может:
- создавать в этом сеансе новые исполнители;
- получать ссылки на исполнители, выполняющихся в этом сеансе;
- получать сервисы из контейнера сервисов, связанного с активным сеансом;
- читать и записывать общие данные активного сеанса;
- прекращать выполнение активного сеанса.
Для каждого активного сеанса создается дочерний контейнер сервисов (DI-контейнер) для области действия, существующей в течение времени существования этого активного сеанса. То есть, экземпляр любого сервиса, зарегистрированного в контейнере сервисов приложения с временем жизни Scoped, полученный из контейнера, связанного с активным сеансом, существует и остается одним и тем же для всех запросов в этом сеансе все время существования этого сеанса, и поэтому может свободно использоваться связанными с сеансом исполнителями. Ссылка на контейнер сервисов активного сеанса хранится в его объекте.
В библиотеке ActiveSession определены сервисы-адаптеры, которые позволяют внедрить в класс обработчика запросов зависимость от экземпляра сервиса, получаемого из контейнера сервисов активного сеанса, а не текущего запроса.
Инфраструктура библиотеки ActiveSession
Инфраструктура библиотеки ActiveSession выполняет все внутренние операции, необходимые для выполнения библиотекой своей работы, приложения напрямую с инфраструктурой не взаимодействуют.
В частности, инфраструктура библиотеки ActiveSession выполняет следующие функции:
- создает или находит существующий объект активного сеанса, к которой принадлежит обрабатываемый запрос и предоставляет ссылку на этот объект обработчикам запроса;
- хранит, отслеживает истечение времени ожидания и прекращает (по истечении этого времени или по вызову из приложения) работу объектов активных сеансов;
- завершает все исполнители, работавшие в завершенном активном сеансе и производит очистку (Dispose) объекта этого сеанса;
- используя фабрики исполнителей (см. далее), зарегистрированные в контейнере сервисов приложения, создает по запросам из объекта активного сеанса новые исполнители, работающие в этом сеансе;
- хранит исполнители, находит запрашиваемые объектами активного сеанса исполнители и возвращает ссылки на них;
- отслеживает истечение времени ожидания и завершает по истечении этого времени работу исполнителей;
- производит очистку завершенных по той или иной причине исполнителей, если для класса исполнителя такая очистка предусмотрена;
Фабрики исполнителей
Фабрики исполнителей — это объекты, которые предназначены для создания исполнителей. Инфраструктура библиотеки ActiveSession получает фабрики исполнителей из контейнера сервисов. Поэтому классы фабрик исполнителей должны быть зарегистрированы при инициализации приложения в контейнере сервисов приложения как реализации того интерфейса, который инфраструктура будет запрашивать из контейнера.
Фабрика исполнителя — это класс, реализующий обобщенный интерфейс IRunnerFactory<TRequest,TResult>
с двумя параметрами-типами. Этот интерфейс содержит единственный метод Create. Параметр-тип TRequest — это тип аргумента, который содержит данные передаваемые в исполнитель. Этот аргумент передается в метод Create этого интерфейса. Параметр-тип TResult — это тип результата для создаваемого исполнителя. Метод Create принимает упомянутый выше параметр типа TRequest с данными, передаваемыми в конструктор исполнителя, плюс ряд дополнительных параметров, используемых инфраструктурой библиотеки ActiveSession, и возвращает интерфейс IRunner<TResult>
созданного исполнителя. В этой статье вопрос создания фабрик исполнителей не рассматривается.
Для регистрации фабрик стандартных исполнителей в составе библиотеки ActiveSession есть предназначенные для этого методы расширения для интерфейса IServiceCollection. Эти методы перечислены в следующем разделе. Для облегчения реализации самописных исполнителей в библиотеке также определены предназначенные для этого вспомогательные классы и методы расширения.
Начало работы с библиотекой ActiveSession
Подключение библиотеки ActiveSession к приложению
Прежде всего, следует включить библиотеку в проект приложения. Делается это обычным образом: установкой пакета NuGet с именем MVVrus.AspNetCore.ActiveSession, в виде которого поставляется библиотека. В частности, этот пакет доступен на nuget.org. Эту, стандартную для установки любого пакета NuGet, операцию я в статье рассматривать не буду.
Выбор классов для исполнителей
Затем нужно определить один или несколько используемых классов исполнителей для приложения. Для этого можно выбрать стандартный класс, уже входящий в библиотеку, либо создать свой. Создание собственных классов исполнителей в этой статье рассматриваться не будет, а описание стандартных классов исполнителей есть в статье, которая дополняет эту, обзорную статью, в соответствующем разделе.
Конфигурирование сервисов
Следующий шаг, который нужно выполнить — добавить в контейнер сервисов на этапе инициализации приложения нужные сервисы. Во-первых, нужно добавить зависимости. Библиотека ActiveSession использует сеансы ASP.NET Core. Поэтому для начала нужно добавить в список регистрации сервисов (интерфейс IServiceCollection, в стандартном начиная с .NET 6 шаблоне WebApplication он доступен через свойство Services экземпляра класса WebApplicationBuilder) нужные для поддержки сеансов сервисы. Минимальный набор действий для этого:
//WebAppBuilder builder;
builder.Services.AddMemoryCache();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession();
Затем в список регистрации сервисов нужно добавить инфраструктурные сервисы библиотеки ActiveSession и сервисы фабрик для выбранных классов исполнителей. Для упрощения регистрации фабрик классов стандартных исполнителей предусмотрены специальные методы расширения списка регистрации сервисов. Методы эти следующие:
-
AddEnumAdapter<TItem>
дляEnumAdapterRunner<TItem>
; -
AddAsyncEnumAdapter<TItem>
дляAsyncEnumAdapterRunner<TItem>
; -
AddTimeSeriesRunner<TResult>
дляTimeSeriesRunner<TResult>
-
AddSessionProcessRunner<TResult>
дляSessionProcessRunner<TResult>
Каждую используемую фабрику исполнителей (в том числе — принадлежащих одному и тому же обобщенному классу, но с разными параметрами-типами для создаваемого стандартного исполнителя) необходимо регистрировать отдельным вызовом соответствующего метода расширения для списка регистрации сервисов.
Любой из этих методов в простейшем случае может быть вызван без дополнительных параметров, примерно так (для примера в качестве параметра-типа всех стандартных исполнителей использован тип String):
//WebApplicationBuilder builder;
builder.Services. AddEnumAdapter<String>();
builder.Services. AddAsyncEnumAdapter<String>();
builder.Services. AddTimeSeriesRunner<String>();
В библиотеке ActiveSession для облегчения регистрации сервисов фабрик самописных исполнителей предусмотрены методы расширения IServiceCollection для их регистрации в контейнере сервисов. Все эти методы являются перегруженными — имеют общее имя AddActiveSession, но различаются параметрами. Описанные выше методы регистрации стандартных фабрик исполнителей на самом деле вызывают именно эти методы. Однако описание этих методов и связанных с ними классов выходит за рамки этой статьи.
Инфраструктурные сервисы библиотеки ActiveSessions регистрируются методом расширения AddActiveSessionInfrastructure для IServiceCollection, его вызов в простейшем случае выглядит примерно так:
//WebApplicationBuilder builder;
builder.Services.AddActiveSessionInfrastructure();
Однако вызывать этот метод отдельно требуется редко, потому что он вызывается автоматически при первом вызове любого метода расширения для регистрации любой фабрики исполнителей — как создающей стандартные исполнители, так и самописной.
Добавление в конвейер веб-приложения обработчика инфраструктуры библиотеки ActiveSession
Последний шаг для подключения библиотеки ActiveSessions к проекту — добавление в конвейер веб-приложения компонента-обработчика (middleware) из этой библиотеки. Делается это на этапе конфигурирования конвейера приложения (middleware pipeline). Но для начала требуется добавить компонент-обработчик для поддержки сеансов ASP.NET Core, которые требуются для работы библиотеки ActiveSession
//WebApplication app;
app.UseSession();
Затем нужно добавить компонент-обработчик библиотеки ActiveSession. Это делается одним или несколькими вызовами метода UseActiveSessions(). Простейшая версия этого метода не принимает дополнительных параметров и вызывается примерно так:
//WebApplication app;
app.UseActiveSessions()
Добавленный ей обработчик предоставляет ссылку в контексте запроса (HttpContext) на объект активного сеанса при обработки всех запросов.
Технически ссылка добавляется через механизм функций (features) и доступна обработчикам через метод расширения GetActiveSession для класса HttpContext.
Более сложные варианты метода UseActiveSessions позволяют отбирать, при обработке каких именно запросов будет устанавливаться ссылка на объект активного сеанса. В этой статье они не рассматриваются.
Технически UseActiveSessions — это метод расширения для интерфейса IApplicationBuilder.
В более сложных его вариантах в него можно передать условие для установки ссылки на на объект активного сеанса: это либо делегат-предикат (получающий как аргумент контекст запроса и возвращающий true или false), либо строка с регулярным выражением для пути запроса. Метод UseActiveSessions, можно вызывать несколько раз. При этом обработчик инфраструктуры библиотеки ActiveSession в любом случае устанавливается только один раз, но ссылка на на объект активного сеанса устанавливается если удовлетворяется условие, переданное в любом из вызовов UseActiveSessions: предикат возвращает true или путь запроса соответствует переданному регулярному выражению. Или же — если был хотя бы раз вызван метод UseActiveSessions без параметров: считается, что его условию удовлетворяет любой запрос.
Использование библиотеки ActiveSession в обработчиках запросов к веб-серверу.
Тема использования библиотеки ActiveSession получилась довольно объемной. Поэтому по ней была написана отдельная, дополнительная статья, где она рассмотрена подробно и с примерами. А в этой обзорной статье приводится только очень краткое описание того, что нужно сделать для использования библиотеки ActiveSession в обработчиках запросов.
Прежде всего, обработчик запроса должен получить ссылку на объект активного сеанса для запроса — интерфейс активного сеанса IActiveSession. Это делается методом GetActiveSession() расширения для класса HttpContext — базового класса для объекта запроса объекта контекста запроса (подробности).
Затем обработчик должен проверить, что объект активного сеанса доступен: свойство IsAvailable интерфейса IActiveSession должно иметь значение true, иначе с этим активным сеансом работать нельзя.
Далее обработчик обычно получает ссылку на исполнитель, с котором он будет работать. Ссылку на исполнитель можно получить двумя способами.
Первый способ — создать новый исполнитель. Исполнитель создается вызовом метода CreateRunner интерфейса активного сеанса. Для стандартных исполнителей из библиотеки ActiveSession определены методы расширения для интерфейса IActiveSession, облегчающие создание этих стандартных исполнителей. Метод CreateRunner возвращает ссылку на вновь созданный исполнитель(его полный интерфейс) вместе с номером созданного исполнителя в текущем активном сеансе (подробности).
Второй способ — найти уже выполняющийся исполнитель по его номеру. Существуют два метода интерфейса активного сеанса для нахождения исполнителя. Эти методы отличаются типом ссылки на исполнитель, которую каждый из них возвращает. Метод GetRunner возвращает полный интерфейс исполнителя, при этом он требует указания типа результата исполнителя. Метод GetNonTypedRunner возвращает часть интерфейса исполнителя, не зависящую от типа его результата. Возвращенный методом GetNonTypedRunner интерфейс нельзя использовать для получения результатов, но можно использовать для других операций с исполнителем, например — чтобы прервать его выполнение. Для большинства стандартных исполнителей определены методы расширения для интерфейса IActiveSession, облегчающие правильное указание типа результата для получения полного интерфейса исполнителя (подробности).
Для получения результатов работы исполнителя в его полном интерфейсе определены два метода. Метод GetRequiredAsync запускает, если это требуется, выполнение фоновой операции в исполнителе и возвращает результат для указанной при его вызове точки выполнения. Что такое точка выполнения — это зависит от типа исполнителя. В качестве примера: точкой выполнения для исполнителя, выбирающего записи из БД, может быть, к примеру, получение в фоновом режиме одной записи. И в таком случае метод GetRequiredAsync возвращает указанное при его вызове число записей из БД. Если фоновая операция ещё не дошла до указанной точки выполнения, то метод GetRequiredAsync асинхронно ждет, когда фоновая операция дойдет до нужной точки (или завершится раньше этого). Метод GetAvailable возвращает результат для указанной при его вызове точки выполнения при условии, что фоновая операция уже дошла до этой точки. Если же фоновая операция до указанной точки не дошла, то метод GetAvailable возвращает результат для последней достигнутой фоновой операции точки выполнения (подробности).
Завершена ли фоновая операция, можно выяснить проверив свойство IsBackgroundExecutionCompleted, а вызвав метод GetProgress можно узнать, до какой точки выполнения дошла фоновая операция. (подробности). Выполнение исполнителя можно прекратить, вызвав его метод Abort (подробности).
Заключительные замечания.
Теперь, когда вы узнали как работать с библиотекой ActiveSession, я могу рассказать подробнее про заглавную картинку к этой статье.
Это — диаграмма, которая иллюстрирует жизненный цикл исполнителя в одном из типовых вариантов его использования.
Обозначения на этой диаграмме — следующие:
- толстые линии со стрелками обозначают основной поток управления веб-приложения: взаимодействия браузера с веб-сервером, приводящие к загрузке веб-страниц и отображение загруженных веб-страниц вместе с исполнением размещенных на них сценариев; в данном примере есть две веб страницы: с одной из них (она отображена частично и подробно не рассматривается) посылается запрос POST на сервер, другая же отображает результат выполнения этого запроса, время от времени подгружая дополнительные данные, полученные исполнителем в фоновом режиме.
- линии средней толщины (тоже со стрелками) обозначают запросы сценария на веб-странице к серверу;
- тонкие линии отображают взаимодействие обработчиков запросов с библиотекой ActiveSession и, в частности, с исполнителем.
- прерывистые и пунктирные линии отображают работу исполнителя: прерывистая линия показывает исполнитель, в работающий в фоновом режиме, а пунктирная — тот же исполнитель, до начала и после завершения фоновой операции.
Жизненный цикл исполнителя в данном примере — следующий:
- Обработчик запроса POST на сервере создает исполнитель (вызовом IActiveSession.CreateRunner), запускает его фоновый процесс и возвращает начальный результат (всё это — вызовом метода IRunner.GetRequiredAsync). Обработчик запроса формирует страницу, отображающую этот результат, и возвращает ее браузеру. Фоновый процесс исполнителя при этом продолжает выполняться на сервере, а инфраструктура библиотеки ActiveSession удерживает в своем хранилище ссылку на этот исполнитель.
- Браузер отображает полученную страницу и выполняет размещенный на ней сценарий, подгружающий дополнительные данные.
- Сценарий с определенным интервалом делает запросы к точке вызова API на сервере с использованием fetch().
- Обработчик точки вызова API находит исполнитель, запрашивает у него текущий результат вместе с состоянием и отправляет их сценарию в браузере в качестве ответа.
- Сценарий, получив ответ, изменяет содержимое страницы, чтобы отобразить полученный результат.
- После первых двух полученных ответов сценарий обнаруживает, что исполнитель ещё выполняется, и планирует следующий запрос к точке вызова API сервера.
- В промежутке между вторым и третьим запросом к точке вызова API фоновый процесс исполнителя завершается. Но это не приводит к переходу исполнителя в конечную стадию, потому что исполнитель ещё не вернул окончательный результат.
- При обработке третьего запроса исполнитель возвращает обработчику точки вызова API окончательный результат и переходит в завершающую стадию. Инфраструктура библиотеки ActiveSession, обнаружив завершение исполнителя, удаляет ссылку на него из хранилища. Теперь объект исполнителя может быть утилизирован сборщиком мусора.
- Получив третий ответ сценарий после отображения результата обнаруживает, что исполнитель завершился и перестает делать периодические запросы к точке вызова API сервера.
Зачем была написана библиотека ActiveSession
Основной моей целью написания всего этого проекта было получить, чисто для себя, оценку, насколько влияют на процесс разработки возможности современных средств разработки, со одной стороны, и современные веяния на тему, как писать код, с другой. Причем, проект, чтобы основанная на нем оценка имела смысл, должен быть достаточно большим.
Я люблю порассуждать сам и посмотреть, что пишут другие на тему, как надо и как не надо писать программы. И имею на это свое мнение. А мнение, я считаю, должно быть основано на чем-то, а именно — на фактах и на опыте. И в какой-то момент я понял, что я не знаю, во что это реально обходятся на практике все эти советы теоретиков, как правильно писать программы, именно в наше время. Мой опыт двадцати-тридцатилетней давности мало что мог подсказать, во что это "правильно" выливается, с одной стороны, при следовании модным теоретическими веяниям, а, с другой — при использовании современных средств разработки. А более свежего опыта у меня не было: мои профессиональные интересы с тех пор сместились от разработки в другую сферу.
В рамках данного проекта в основном меня интересовало не само написание выполняющего реальную работу кода, а а всяческая обвязка: реализация ведения подробного журнала (оно же "логирование"), написание тестов, документации и т.п.
Конечно, я время от времени делал всякие мелкие программки, но я хорошо осознавал, что нужного понимания на их основе не получить. На маленьких учебных примерах новых технологий и новых веяний можно только понять, как всем этим пользоваться, но — не насколько это помогает/во что все это обходится: компенсируют ли повысившиеся возможности средств разработки излишние затруднения, вносимые этими самыми новыми веяниями. Поэтому для понимания мне понадобился достаточно большой проект. А поскольку программист я не настоящий и не имею возможности заняться таким делом за счет работодателя — т.е. убедить его начать новый большой проект, причем, по сути — ради моего любопытства — то у меня остался только один выход — заняться самодеятельностью (нынче это называется "pet project"). Благо это занятие требует всего лишь времени, которого у меня, к счастью, хватает. Ну, и компьютера — но с этим сложностей сейчас нет.
Следующий потребовавший решения вопрос — а что за проект делать? Общий выбор области для проекта для меня был однозначен — та область, в которую я сейчас углубился: серверная часть веб-приложения (AKA back end). По тем же соображениям было выбрано и средство разработки: ASP.NET Core. Остался только вопрос содержимого: что именно должен делать проект? Нужный для оценки объем, в принципе, было бы несложно набрать и на каком-нибудь типовом полу-учебном веб-приложении, если привесить к нему бантиков, свисточков и плюшевых кубиков. Однако делать очередной никому не нужный ежедневник/интернет-магазин и тому подобную скучную чепуху откровенно не хотелось. Идей для стартапа у меня отродясь не было, так что этот путь совмещения приятного (то есть денег) с полезным тоже отпадал.
В поисках идеи я вспомнил одну статью, которую я прочитал где-то за год до того. Мысль развить изложенную в ней идею мне показалась полезной.
Точнее — статей было две, вот вторая. Проблема, которую взялся решать автор этих статей, по моему опыту, отнюдь не была несуществующей: я сам в своей практике видел достаточно любителей запросить всё ("огласите весь список, пожалуйста"©, ага), а потом медитативно это всё просматривать, ища нужное глазами, ещё в те времена, когда программировал на Delphi. Для программ на Delphi(+BDE) такой стиль работы был терпим, хоть и не всегда (например, с MS SQL Server с его блокировками так работать не стоило, а вот с Interbase, с его многоверсионной системой хранения — запросто). Потому обеспечение такой схемы работы с веб-приложением — выбрать начальную часть данных, а остальное получать и возвращать в фоновом режиме в ответах на дополнительные запросы выглядело вполне разумным, хотя и далеким от нынешней моды решением.
А ещё при чтении этой статьи я обратил внимание на явный минимализм решения: все в нем было сделано из подручного материала, по принципу "я тебя слепила из того, что было". Это никоим образом не упрек автору — статьи были не про теорию, а про реальную жизнь а в реальной жизни обычно именно так и приходится работать: использовать то, что есть под руками. Но я уже тогда сразу подумал, что неплохо было бы допилить эту идею до полноценной библиотеки, с развесистой, по современной моде, архитектурой и использованием дополнительных возможностей ASP.NET Core из тех, что не особенно рекламируются.
И я решил, что библиотека, реализующая функциональность, описанную в статье — поддерживающая хранение состояния сеанса взаимодействия с конкретным пользователем и выполнение кода сеанса между относящимися к сеансу запросами — это хороший ответ на вопрос о содержании проекта. Да, этот проект не будет стильным, модным и молодёжным. Да, он будет лежать в стороне от магистрального пути развития современных веб-технологий. Да, он не сделает меня основателем популярного Open Source проекта с мировым именем. Но я смогу сделать достаточно большой проект, не являющийся заведомо бесполезным хотя бы в моих глазах. А если он пригодится кому-либо ещё — тем лучше.
В целом, архитектуру библиотеки ActiveSession я в этой, обзорной, статье решил не описывать — ибо нельзя объять необъятное.Однако какие-то слова про нее и про отличие ее от архитектуры из статьи, породившей идею, написать хотелось. И как компромисс, я про архитектуру некоторое количество слов написал, но эти слова погребены здесь, в скрытом тексте, так что мешать обзору они не будут.
Если оценивать из общих соображений, решение для хранения состояния активного сеанса — включающего в себя несколько запросов, при том, что в рамках сеанса может выполняться активный код между запросами — должно иметь определенный набор функций. Основные из них:
- привязка каждого приходящего запроса к объекту сеанса — существующему или вновь созданному;
- хранение между запросами ссылки на объект состояния сеанса и на объекты, которые выполняются в рамках сеанса в фоне, дабы они не стали добычей сборщика мусора;
- предоставление доступа к объекту сеанса обработчикам входящих в него запросов;
- возможность запуска нового кода в сеансе и взаимодействия с ранее запущенным кодом;
- своевременное освобождение объектов — объекта самого сеанса и других связанных с ним объектов — очистка их (вызовом методов Dispose/DisposeAsync, если они реализуют соответствующие интерфейсы) и удаление ссылок на них;
- с учетом того, насколько широко в ASP.NET Core используются сервисы и внедрение зависимостей от них — иметь в сеансе свой контейнер сервисов для области действия, связанной с этим сеансом.
В библиотеке из статьи решение этих задач строилось вокруг контейнера сервисов. Для каждого сеанса создавался дочерний контейнер сервисов для области сеанса. Получал объекта сеанса и связывал его с запросом добавленный в конвейер объект-обработчик (middleware). Привязка запроса выполнялась на основе переданного в нем значения куки — идентификатора сеанса. Связь запроса с сеансом устанавливалась через специальный сервис, интерфейс которого позволял получить доступ к контейнеру сервисов сеанса. Этот сервис имел время жизни области запроса, а реализация его интерфейса для этого запроса настраивалась упомянутым выше компонентом-обработчиком middleware. По замыслу библиотеки из статьи, объект, содержащий специфичные для сеанса данные, и методы, выполняемые в сеансе между запросами, регистрировался в приложении как сервис с временем жизни ограниченной области. Обработчик запроса должен был получать ссылку на этот, общий для всего сеанса, объект из контейнера сервисов, связанного с сеансом. Объект сеанса (в простейшем варианте, на момент публикации статьи, это был просто дочерний контейнер сервисов для области сеанса) помешался в стандартную реализацию кэша, доступную через общий для приложения сервис IMemoryCache под ключом сеанса, передаваемым через куки. Кэш автоматически отслеживал заданное допустимое время существования сеанса. Очистка сеанса производилась функцией обратного вызова, срабатывающей при удалении объекта из кэша. При этом вызывалась очистка дочернего контейнера кэша области сеанса, что автоматически приводило к очистке всех связанных с сеансом объектов-сервисов и удалению ссылок на них.
Короче, у автора статьи получилась весьма логичная и компактная по объему кода структура библиотеки, активно использующая при этом встроенные возможности ASP.NET Core. Но этот минимализм имел и оборотную сторону. Например, реализуя объект с кодом, выполняющимся в сеансе, как сервис, приходится отказываться от возможности легко и просто запустить в сеансе несколько экземпляров таких объектов с разными параметрами. Запустить и выполнять одновременно несколько фоновых операций можно, только если они выполняются разными сервисами. Но при этом затруднено раздельное управление этими фоновыми операциями. Например, поскольку жизненный цикл всех объектов, реализующих сервисы — когда их создавать, как долго хранить ссылку на них и когда выполнить очистку — определяется контейнером сервисов, то если одна из одновременных фоновых операций завершится, то невозможно создать новый объект того же типа, чтобы запустить с его помощью новую такую же операцию. В общем, гибкость решения была разменяна на его простоту.
Я в разработке своей библиотеки ActiveSession ограничениями по простоте реализации и объему кода связан не был (скорее, наоборот, см. выше). Поэтому я позволил себе принимать решения в интересах универсальности и гибкости.
Прежде всего, объект библиотеки ActiveSession, выполняющий код в рамках активного сеанса, больше не является сервисом, регистрируемым в контейнере сервисов, а потому не скован ограничениями, налагаемыми контейнером. Это позволило избавиться от описанных выше ограничений: один экземпляр объекта на сеанс, невозможность независимого управления выполняющими код объектами и пр. Таким образом, появилась концепция исполнителей, реализующих вновь разработанный интерфейс IRunner (он описан в статье — здесь и далее под словами "в статье" имеется виду и эта, и дополнительная статья).
Для взаимодействия объекта сеанса библиотеки ActiveSession с такой новой реализацией выполняющих код в сеансе объектов — исполнителей, вместо интерфейса контейнера сервисов (IServiceProvider) был разработан новый интерфейс, IActiveSession, (он тоже описан в статье), дающий больше возможностей возможностей, и создан реализующий его объект инфраструктуры.
Для доступа к объекту сеанса из обработчиков запросов был использован стандартный для ASP.NET Core, но при этом не особо известный механизм расширения контекста запроса (HttpContext) — механизм функций (Features). Этот механизм позволяет хранить в контексте запроса в специальной коллекции функций — свойстве Features — набор произвольных интерфейсов для реализующих эти функции объектов и извлекать их по ключу — типу интерфейса. Компонент-обработчик конвейера обработки запроса (middleware) добавляет в коллекцию функций контекста объект, реализующий интерфейс IActiveSessionFeature. Свойство ActiveSession этого интерфейса предоставляет ссылку на объект сеанса. Обработчики запросов могут получать ссылку на объект активного сеанса вызовом функции расширения GetActiveSession() для контекста запроса: он находит в коллекции функций интерфейс IActiveSessionFeature и возвращает значение его свойства ActiveSession. Объект, реализующий интерфейс функции создается обращением компонента обработчика к хранилищу ( о нем будет чуть позже). Хранилище находит существующий или создает новый объект активного сеанса и создает реализующий интерфейс функции объект, который описанным выше образом возвращает ссылку на объект этого активного сеанса.
Для привязки запросов к активному сеансу было решено не повторять свой велосипед на основе куки, как было сделано в исходной в статье, а использовать стандартный механизм сеансов ASP.NET Core, предоставляющий интерфейс ISession. механизм этот, конечно, работает через те же самые куки, но он имеет при этом дополнительные возможности. Во-первых, сеанс ASP.NET Core позволяет хранить в своей инфраструктуре дополнительные статические данные (вообще говоря — любого типа, сериализуемого в массив байтов, при этом для хранения чисел и строк есть встроенная поддержка в виде методов расширения). Во-вторых, сеансы в ASP.NET Core, в принципе, являются распределенными: если приложение выполняется параллельно на нескольких узлах, то можно настроить механизм сеансов так, чтобы на всех этих узлах данные сеанса ASP.NET Core были одинаково доступны.
Первая возможность — сохранять дополнительные данные — инфраструктурой библиотеки используется, но зависимость от нее не критична: при использовании своей куки вместо сеанса их эти данные можно было бы хранить в куки. Вторая возможность — распределенный сеанс — теоретически позволяет реализовать поддержку распределенного активного сеанса, но так как напрямую сам выполняющийся код в распределенный сеанс ASP.NET Core запихнуть невозможно, то эта реализация требует дополнительной работы. Эту работу я оставил на потом, для какой-нибудь следующей версии. Пока что в библиотеку ActiveSession просто в поиск существующего исполнителя добавлена проверка того, что найденный исполнитель, выполняется на другом узле, которая, в зависимости от настроек, либо выбросит исключение, либо вернет результат "исполнитель не найден".
Хотя в комментариях к исходной статье я предлагал на роль хранилища ссылок на объекты активных сеансов не кэш в памяти(общий IMemoryCache из контейнера сервисов или поддерживающий этот интерфейс отдельный объект MemoryCache), а другой стандартный механизм .NET — механизм параметров (Options pattern), но в зрелом размышлении я согласился с тем, что выбор кэша в памяти как основы хранилища был оптимальным. Потому что важной задачей хранилища является удаление устаревших объектов сеанса — а кэш в памяти специально ориентирован на решение именно этой задачи.
Хранилище в библиотеке ActiveSession — это отдельный, довольно сложный объект, а не просто кэш в памяти, доступный как сервис через контейнер. Потому что круг задач, решаемых хранилищем, в библиотеке ActiveSession гораздо шире. Во-первых, объекты исполнителей имеют свое время существования. В частности, при отсутствии обращения к ним они тоже могут стать устаревшими, причем — стать раньше, чем объект сеанса, в котором они выполняются. А потому исполнители — тоже хорошие кандидаты на хранение в кэше в памяти. Во-вторых, при удалении объекта активного сеанса из кэша все связанные с ним объекты исполнителей должны тоже быть удалены из кэша и очищены. То есть, у кода хранилища опять-таки появляется дополнительная работа. И раз уж все равно требуется достаточно сложный объект хранилища, то в него разумно и перенести код создания активных сеансов, исполнителей и других объектов библиотеки ActiveSession. Поэтому хранилище в библиотеке ActiveSession — это центральный объект инфраструктуры библиотеки, вокруг которого строится вся работа по созданию, хранению и освобождению объектов, используемых при работе с библиотекой. Но поскольку эта работа пользователю библиотеки не видна, то хранилище, как и другие объекты инфраструктуры, остались вне предмета рассмотрения данной статьи. Единственный компонент инфраструктуры, который был упомянут в статье — это фабрики исполнителей. Потому что эта часть инфраструктуры пользователю библиотеки видна: для создания нужных пользователю исполнителей он должен регистрировать в контейнере сервисов фабрики, которые их создают.
Кроме того, библиотека ActiveSession предоставляет расширенные возможности наблюдения за жизненным циклом объектов, находящихся в хранилище — активных сеансов и исполнителей: можно наблюдать как момент завершения выполнения и удаления объекта, так и момент завершения его очистки и настроить в приложении обработчики этих событий (об этом тоже написано в статье).
Лицензия.
Открытая, конкретно — лицензия MIT. "Берите люди, пользуйтесь."(с) Зарабатывать деньги на этой библиотеке я не планирую.
Потому что движение за "свободное ПО", так же, как и любое движение за свободу чего-нибудь и кого-нибудь, в лучшем случае — лицемерно, а худшем — одно из тех благих намерений, которыми, говорят, дорога в ад вымощена. Конкретно же, борьба за "свободное ПО" против "проприетарного" привела по факту к свободе для корпораций невозбранно гадить нам в мозги своей рекламой, да ещё и пользуясь для этого не ими разработанным ПО. Но это — тема совсем для другой статьи.
Ограничения.
Нынешняя версия библиотеки ActiveSession содержит некоторые ограничения функциональности.
1.Все запросы одного активного сеанса должны обрабатываться на одном узле веб-фермы/кластера. Ограничение это — наверное, самое заметное, ибо требует специальных настроек для использования текущей версии ActiveSession в приложении, выполняющемся параллельно на нескольких узлах. А именно — привязки сеансов ASP.NET Core (которые являются основой активных сеансов библиотеки ActiveSession) к узлам с использованием куки (другое название — sticky sessions). То есть, оно требует, во-первых, балансировщика нагрузки перед веб-фермой ("круговой" балансировки через DNS недостаточно), а, во-вторых, чтобы балансировщик поддерживал такую возможность (к примеру, в широко используемом в качестве балансировщика обратном прокси-сервере NGinx эта возможность AFAIK есть только в платной версии).
Причина трудности очевидна: если данные несложно реплицировать между узлами (в частности это делает реализация сеансов ASP.NET Core на базе распределенного кэша), то код исполнителя активного сеанса выполняется на конкретном узле и реплицировать его никуда нельзя. Так что, если запрос приходит на обработку на другой узел, то обработчик должен вызвать исполнитель с того самого другого узла. С одной стороны, это вполне реализуемо. Причем в простом, костыльном, варианте — не так уж и сложно: на узле, где выполняется исполнитель, создается специальная точка вызова HTTP API, принимающая запросы к исполнителю, а на узле, где выполняется обработчик, на время обработки запроса создается прокси-объект, который обращается к исполнителю через эту точку вызова.
Но этот вариант — именно, что костыльный. Во-первых, создание прокси-объекта для каждого запроса может быть накладным, поэтому желательно уже созданный прокси-объект сохранять в инфраструктуре библиотеки (в хранилище) для повторного использования и очищать его, когда необходимость в нем отпадет. Во-вторых, для общения между экземплярами серверной части приложения на разных узлах уже могут использоваться другие механизмы — очереди сообщений, шины и т.п., и было бы целесообразно для обращения к исполнителю использовать не простой такой вот "RPC over HTTP", прибитый к реализации гвоздями, а эти, уже имеющиеся механизмы транспорта. То есть, требуется усовершенствование архитектуры библиотеки: добавление абстрактного транспорта для запросов к исполнителям на других узлах, и реализация этого абстрактного транспорта для конкретных механизмов (хотя бы части их). Таким образом, потребуется дополнительная работа по усовершенствованию библиотеки. Может быть, когда нибудь, эта работа будет проделана, а пока что имеем это ограничение. Впрочем, интерфейсная часть библиотеки уже позволяет писать приложение так, будто его исполнители реально работают на других узлах: для этого для интерфейса IRunner уже предусмотрены методы расширения, формально выполняющие запросы к исполнителю асинхронно (что требуется для работы с исполнителем на удаленном узле через некий транспорт).
2.Только один действующий активный сеанс на одного пользователя (сеанс ASP.NET Core), имеющий единственный общий контейнер сервисов для всего активного сеанса.
Из-за этого возможно совершенное излишнее влияние одних частей приложения на другие через общие используемые сервисы. Проблема тут в том, что существуют широко используемые сервисы, которые не рассчитаны на одновременный доступ к ним из нескольких параллельно выполняющихся потоков выполнения кода. Самые, наверное, важные из них — это контексты Entity Framework (классы — потомки DbContext). Использовать такие сервисы в исполнителях ActiveSession можно, но это требует использования объектов взаимоисключающего доступа, чтобы избежать параллельного доступа к экземпляру такого сервиса (этот механизм описан в дополнительной статье, более подробно рассказывающей о работе с библиотекой ActiveSession). Там, где такое влияние обусловлено логикой работы приложения, от этого никуда не деться. Но вот от ненужного влияния не связанных друг с другом разных частей приложения через сервисы одного и того же типа стоило бы избавиться. И создание разных активных сеансов для разных частей приложения — это один из способов.
В приложении ASP.NET Core эти сервисы регистрируются как имеющие время жизни области действия (Scoped). При штатном использовании их в обработчиках запросов HTTP, с каждым из которых связана своя область действия, для каждого запроса создается свой экземпляр сервиса, который очищается по завершении обработки запроса. Таким образом параллельный доступ к одному экземпляру невозможен (по крайней мере, его несложно избежать, если не делать ошибок).
Используемый в исполнителях экземпляр контекста EF получается из контейнера сервисов активного сеанса, а потому невозможность параллельного доступа к нему в текущей версии сама по себе не гарантируется: в активном сеансе могут выполняться параллельно несколько запросов. Поэтому приложение должно использовать объекты взаимоисключающего доступа для работы с контекстами EF. Ну, или самому как-то следить за этим.
Впрочем, для предотвращения проблем конкретно с контекстами EF можно использовать средство, предназначенное в EF именно для таких случаев: фабрику контекстов — зарегистрировать в контейнере сервисов именно фабрику и получать контексты через нее. А освободить полученный от фабрики контекст можно по завершении исполнителя — как это можно сделать, рассмотрено в той же дополнительной статье.
3.Поскольку исполнители на основе класса, в том числе — стандартные, создаются сейчас с использованием ActivatorUtilities.CreateInstance, то, если этот класс имеет конструктор с атрибутом [ActivatorUtilitiesConstructor], то для создания исполнителя можно использовать только этот конструктор. Для классов стандартных исполнителей этот атрибут не используется, но при создании самописных исполнителей это надо иметь в виду. Хотя в этой статье про создание самописных исполнителей ничего не написано, я решил, тем не менее, упомянуть и об этом ограничении.
Об отладке и тестах.
Раз уж я решил следовать веяниям современной моды в программировании, то пройти мимо тестирования, а особенно, самой формализуемой его части — модульных тестов я никак не мог. Вот я и не прошел.
Не то, чтобы я считал тесты однозначным излишеством: весь мой опыт программирования с тех древних времен, когда я только начинал это делать, говорил мне, что по жизни непроверенная и неотлаженная программа работать без ошибок будет только чудом.
… и я одно такое даже видел. Был у меня кусок кода размером под три сотни строк, написанный в один присест. Причем — кода непростого: там нужно было вырезать из строки подстроку не совсем простой структуры, причем — не пользуясь регулярными выражениями и прочими замечательными средствами, упрощающими и ускоряющими работу. Потому что эти средства любят выделять себе немного памяти почем зря, а потом сборщик мусора устает прибирать за ними. Короче — только StringBuilder и только простые методы поиска, вроде IndexOf. Просчитаться на единицу в таком коде — раз плюнуть. Вообще-то, в самом по себе решении этой задачи ничего чудесного не было, чудо там было другое: этот код заработал правильно с первого раза, и сколько я потом ни искал в нем ошибку (а я искал, и долго — потому что ее отсутствие было бы чудом, а я на чудеса полагаться опасаюсь) — не нашел. Так что, чудеса бывают, убедился.
Но на чудеса я не полагаюсь. Так что проверку и, при необходимости, отладку программы я считаю своим долгом.
Тесты — дело формальное. Их написание порождает материальные следы ("артефакты"). Тогда как проверка может проводиться даже просто заданием значений в отладчике, делаться совершенно неформально и следы она за собой оставлять не обязана. Тестирование имеет метрики, которые менеджер может измерить и даже записать их в KPI программистам. Короче, если по жизни, то проверка — это для себя и для дела, чтобы было нормально, а тесты — для менеджеров, чтобы они могли принести их жертву богу управляемости, которому они поклоняются. Объем проверки определяет сам программист на основании своего опыта, а покрытие тестами — менеджеры, согласно заветов своих менеджерских богов и пророков.
Но раз я стал делать библиотеку "как положено", то тесты я тоже начал писать в должном количестве. Кому интересно — тестовый проект, ActiveSession.Tests, лежит в репозитории рядом с исходными текстами библиотеки. Средства для тестов использовались современные: xUnit и Moq для написания и Test Explorer из Visual Studio для прогона. И внезапно выяснилось, что именно в случае библиотеки эта инфраструктура тестов — очень удобное средство и для проверки и отладки кода библиотеки. Так что с некоторых пор писать тесты конкретно для библиотек, разрабатываемых в Visual Studio рекомендую.
Потому что библиотека — не приложение, ее просто так на выполнение не запустишь. И потому раньше надо было для проверки и отладки чего-нибудь в библиотеке делать вспомогательное приложение, которое это что-нибудь использовало способом, нужным для проверки, и это приложение проверялось и, при необходимости, отлаживалось. А при наличии инфраструктуры тестирования можно вместо этого приложения использовать тесты: инфраструктура тестирования позволяет весьма удобно запускать куски кода библиотеки прямо в Visual Studio, в том числе — и под отладчиком. Так что, внезапно, средство для менеджеров оказалось пригодным и для программиста. В результате я с некоторого момента забросил пробное приложение (но оно осталось в репозитории с примерами) и для проверки и отладки перешел исключительно на инфраструктуру тестирования Visual Studio. Оборотной стороной такого перехода, однако, стало то, что тесты делались для потребностей проверки конкретной реализации и потому оказались сильно привязанными к этой реализации, т.е. приобрели излишнюю хрупкость. А ещё при таком процессе проверки оказалось, что громадное большинство найденных ошибок составляли ошибки в самих тестах, а не в коде библиотеки.
Так или иначе, написание тестов с покрытием, близким к столь желанным менеджерам 100%, дало мне некие цифры на тему того, во что тесты обходятся. Конкретно для библиотеки ActiveSession тесты обошлись дорого.
Согласно средствам анализа кода Visual Studio, на 2060 строк кода библиотеки у меня пришлось 5651 строк кода тестового проекта — т.е. в два с лишним раза. И это — несмотря на предпринятые меры по уменьшению дублирования в коде тестов: вынос инициализации тестов и части проверок во вспомогательные классы и методы и т.д. Правда, с другой стороны, код библиотеки оказался значительно более сложным и куда менее линейным, нежели код тестов: посчитанная Visual Studio цикломатическая сложность у проекта библиотеки оказалась, наоборот, выше почти в два раза, чем у тестового проекта: 1841 против 908. Общее количество строк в исходных файлах библиотеки тоже оказалась выше (13994 против 10556), но здесь сказался не только более плотный линейный код тестов без многочисленных строк с фигурными скобками во всяких условных операторах, циклах и пр., но и большое количество в исходном тексте библиотеки "трехслэшовых" комментариев, содержащих текст для XML-документации.
Наблюдаемость AKA observability.
Из трех столпов наблюдаемости в библиотеке ActiveSession лучше всего обстоят дела с журналированием (logging). Библиотека использует стандартную подсистему журналирования .NET (ILogger/ILoggerFactory) со всеми присущими ей возможностями в плане подключения поставщиков, поддержки структурного журналирования и конфигурации. В частности, в библиотеку встроено очень подробное трассировочное журналирование — которое, однако, можно не только отключить через конфигурацию, но и убрать из кода начисто, если не определять при сборке символ TRACE. У MS есть поставщик журналирования, совместимый с Open Telemetry, так что можно перенаправить журналирование и туда: это стильно, модно и молодежно, а ещё и бесплатно.
С метриками хуже. Из библиотеки можно получить только рудиментарные метрики хранилища через совершенно нестандартный интерфейс — и все.
А на тему распределенной трассировки в библиотеке и конь не валялся. Хотя бы потому, что она сама по себе — не распределенная.
Документация и примеры.
Делать документацию я решил на уровне "для себя", такой, какой бы я хотел ее видеть, если бы я был не автором библиотеки, а ее пользователем.
В процессе работы я начал использовать Moq и долго страдал от того, что широко известная документации него него — одна страничка с примерами. Впрочем, потом я нашел и документацию по его классам и методам — похоже, собранную какой-то программой из "трехслэшовых" комментариев в исходном тексте, и эта документация таки оказалась для меня весьма полезной — подсказки в IDE, хотя и берутся из того же источника, слишком фрагментарны, чтобы увидеть полную картину.
Так что документация есть, и почти такая, какую я хотел. Язык документации — только английский, потому что из-за трудоемкости написания документации пришлось выбирать один язык.
Документация включает в себя, прежде всего README для пакета NuGet библиотеки. Его содержимое в основном совпадает с содержимым этой и дополнительной статьи (без вступления, заключения и большей части скрытого текста — но примеры и некоторые описания концепций, которые попали в статьях в скрытый текст, там есть).
Кроме того, на GitHub Pages лежит сайт документации, автоматически сформированный программой DocFx из "трехслэшовых" комментариев для документации в формате XML. Ценность в нем на текущий момент представляет только сама документация по интерфейсам, классам, их методам и свойствам. Всё остальное там — пустой шаблон от DocFx, в который я, однако, планирую перенести содержимое README, разбив его на статьи.
Имеется также репозиторий с примерами использования библиотеки ActiveSession: ссылка на него была в начале статьи. В частности, там лежит исходный код программы, демонстрирующей примеры использования всех стандартных исполнителей библиотеки ActiveSession и в дополнение к ним — использование функции взаимоисключающего доступа к сервису из контейнера активного сеанса.
Перспективы.
Тут многое зависит от того, будет ли этой библиотекой кто-нибудь пользоваться. Потому что лично я задачи, которые ставил перед собой, делая этот проект, считаю выполненными: опыт — получен, цифры — измерены, выводы — сделаны. В принципе, есть вещи, которые я не доделал, но хотел бы — я про них по тексту писал. Их я, наверное, сделаю.
Ну, а если проектом будет кто-то пользоваться, то, по крайней мере, уж баги-то (а они обязательно будут) я по мере обнаружения буду устранять: долг чести, я считаю. Остальное — как получится. Короче, use at your own risk, как говорится.
Ну вот, собственно, и всё, о чем я хотел рассказать в статье. Благодарю тех, кто осилил.
А ещё здесь должна бы быть ссылка на мой Telegram-канал, но у меня нет Telegram-канала. Так что если есть вопросы и пожелания — пишите в комментариях к этой статье и в личные сообщения мне здесь на Хабре. А ещё можно использовать инфраструктуру общения с авторами на nuget.org и github.com.
Комментарии (36)
posledam
27.10.2024 17:47Глубоко в детали пока не углублялся, но вот чего я не нашёл в содержании, это какого-то исследование альтернатив, или инструментов, которые можно было бы использовать для достижения той же цели. И, самое главное, чем данное конкретное решение лучше.
На поверхности сразу: SignalR, сессия итак создаётся на каждое подключение и держится до его окончания, разные вкладки можно объединять в группы (например, по user-id) и поддерживать процессы для группы. Чем не подходит обычный IHostedService в связке с SignalR и удержание сессионного CancellationTokenSource?
Если говорить про масштабирование вычислений, а это нам точно понадобится, то почему не Orleans? Как-то грустно, когда бекенд, который хостит апишечеку, должен ещё чего-то в фоне делать, да ещё и с лютейшими ограничениями на прилипание пользователя к конкретному экземпляру.
В общем, вопросов много, ответов мало. Если это просто ради опыта, то опыт безусловно это хорошо, но настоящий инженер перед тем, как изобрести колесо, сначала изучит другие колёса.
mvv-rus Автор
27.10.2024 17:47Глубоко в детали пока не углублялся, но вот чего я не нашёл в содержании, это какого-то исследование альтернатив
А его там и нет. Цель статьи - описание библиотеки, а не ее продвижение. Сама библиотека, по факту - это побочный продукт другой деятельности, про это в статье есть, в конце. Но раз написана - пусть будет, вдруг кому пригодится.для его конкретной ниши.
posledam
27.10.2024 17:47Я не пытаюсь обесценить ваши труды. Но всё же, какого-то хоть минимального исследования проблемы и существующих решений очень сильно не хватает для полноценной статьи.
Иначе выглядит это так:
-- Смотрите, какой я написал свой логгер / парсер / запускатель задач / etc..
-- А вы в курсе, что уже есть как минимум решения X, Y, Z... решающих ваши задачи?
-- А зачем? Чукча не читатель, чукча писатель:)
mvv-rus Автор
27.10.2024 17:47Чукча не читатель, чукча писатель
Это было сказано в другом контексте: про отсутствие необходимых базовых знаний для обучения профессии. Здесь случай, IMHO другой, Базовые знания, как писать код большими кусками, у меня, смею надеяться, есть (думаю, достаточно даже этой библиотеки, чтобы подтвердить). А в качестве части моего CV на должность техлида - которому по должности как раз положено выбирать средства для использования проекта - эта статья не предназначена.
Полноценность же статьи зависит от задачи. Здесь я ставил задачу полноценно описать получившееся у меня решение, не более того. Добавлять сравнения с альтернативами - это увеличивать объем статьи, а статья и так получилась слишком большой ("редкая птица долетит до ее середины" :-) , это по статистике, любезно предоставленной Хабром, видно).
По предыдущему комментарию дополню: билиотека предназначена для сохранения состояния и выполнения фоновой работы для одного конкретного пользователя. В hosted services из коробки средств для этого AFAIK нет. А вот SignalR как альтернативное средство поддержания сессий, я, возможно, попробую интегрировать. В конце концов, от такого средства библиотеке мало что нужно, в основном - key/value хранилище. А то, что в нем нет HttpContext - это лечится наследованием своего класса (я посмотрел - вроде бы, мешающих этому ограничений нет, сейчас - это вам не ASP.NET Framework).
PS И не бойтесь говорить мне прямо, то, что вы думаете. Меня это задевает не сильно. Ибо в Интернет я пришел ещё в те времена, когда про него говрили "Это Интернет, детка! Здесь могут посласть". И, что характерно, посылали. А до этого у меня в анамнезе были 90-е, хоть и в легкой форме.
posledam
27.10.2024 17:47Вы зря так реагируете.
В Hosted Services есть всё, что нужно. Это фоновый процесс, который может запускать и останавливать задачи, по одному на каждую сессию. Для коммуникации с фоновым процессом можно использовать простейший диспетчер, например, Task Channels, что также идёт из коробки.
SignalR напрямую решает задачу поддержания активной серверной сессии для каждого клиента, так как это требуется для двух-сторонней коммуникации и обмена данными.
Итого, вы не потрудились провести даже минимальное исследование платформы. Это даже не какие-то "левые" библиотеки, это всё идёт в коробке. Но написали очень много кода.
Зачем? :) Ну возможно в этом и есть смысл, возможно действительно что-то из представленного не решает тех проблем, с которыми столкнулись именно вы. Но какие это проблемы?
Разработка ПО это прежде всего инженерная дисциплина. Писать много кода -- это не значит хорошо, чем меньше кода мы пишем для достижения желаемого, тем эффективней наш труд, тем выше его ценность.
Какого-то анализа правда не хватает. Зачем писать свой велосипед на каждый чих? Потому что могу? :)
mvv-rus Автор
27.10.2024 17:47В Hosted Services есть всё, что нужно. Это фоновый процесс, который может запускать и останавливать задачи, по одному на каждую сессию.
Может. Но кто будет отслеживать сессии, и в частности - судьбу задач: сессия-то может кончиться внезапно, и кто тогда таймаут проконтролирует? Добавить в требования SignalR - а как быть тем, кто модифицирует уже написанное, SignalR - штука довольно специифическая. Короче, "здесь не всё так однозначно".
Для коммуникации с фоновым процессом можно использовать простейший диспетчер, например, Task Channels, что также идёт из коробки.
Не нашел такого. В том числе - и поиском: утка не ищет. Не кините ли ссылку, чтобы было понятно, о чем речь.?
Но написали очень много кода.
Зачем? :)Дык, в статье написано - зачем. Не читали, признайтесь?
И требовалось мне (ну, или хотелось, начальства-то надо мной никакого нет, заказчиков - тоже) как раз написать много кода (а ещё - много тестов и много документации).
Ну, а получившаяся библиотека - считайте ее побочным продуктом.Если вам она не подошла - ну, значит, вам она не нужна. Но, может, кому-нибудь сгодится: для того и статья.
Разработка ПО это прежде всего инженерная дисциплина. Писать много кода -- это не значит хорошо, чем меньше кода мы пишем для достижения желаемого, тем эффективней наш труд, тем выше его ценность.
А если речь идет не о коммерческой разработке, а, к примеру, just for fun - тогда как?
mayorovp
27.10.2024 17:47Не нашел такого. В том числе - и поиском: утка не ищет. Не кините ли ссылку, чтобы было понятно, о чем речь.?
Потому что "Task" тут лишний.
mvv-rus Автор
27.10.2024 17:47Не ожидал, что диспетчером можно назвать простую реализацию шаблона "производитель/потребитель" - хотя бы потому, что она сам по себе этот шаблон ничего никуда не распределяет, нужен некий распорядитель (его ещё брокером называют).
nronnie
27.10.2024 17:47В Hosted Services есть всё, что нужно.
Сам по себе Hosted Service имеет только очень базовый функционал для выполнения кода в фоне. Если же вам нужно еще как-то с этим кодом взаимодействовать, управлять его запуском или остановкой, или выполнением в каком-то контролируемом числе потоков - то все это вам придется делать самим, и это уже проходили. В готовых же frameworks все это уже есть. А под капотом там как раз и есть Hosted (Background) Service, просто уже с различными готовыми "добавками".
mvv-rus Автор
27.10.2024 17:47И ваш любимый планировщик Quartz интегрирован в ASP.NET Core именно как Hosted service, я правильно понимаю (если нет - поправьте),
rukhi7
27.10.2024 17:47Тесты — дело формальное. Их написание порождает материальные следы ("артефакты"). Тогда как проверка может проводиться даже просто заданием значений в отладчике, делаться совершенно неформально и следы она за собой оставлять не обязана. ...
А ведь так оно и есть! Только за эту формулировку можно плюс поставить, по моему.
mayorovp
27.10.2024 17:47Не нравится мне опора на UseSession. Состояние сеанса в ASP.NET - это целое хранилище сериализуемых данных, которое может храниться в куче мест, включая память, реляционные СУБД и redis. И из всего этого комбайна вам требуется... что? Одно единственное значение или вообще идентификатор сессии?
Вторая проблема подобной связи - во времени жизни. Время жизни фоновой операции совершенно не обязано совпадать с пользовательским сеансом, и может отличаться в обе стороны. Некоторые операции (скажем, загрузка файла с регулярными отчётами о прогрессе) актуальны только в том окне, где были инициированы, и весь пользовательский сеанс для них - слишком много. Другие операции (скажем, синхронизация с другой системой) актуальны для инициировавшего их пользователя или даже для группы пользователей, и пользовательский сеанс для них - слишком мало.
Вообще, я в своей практике ещё ни разу не встречал таких данных, которые и правда были бы уместно хранить в состоянии сеанса. Он всегда либо слишком большой, либо слишком маленький, и его использование - всегда костыль.
mvv-rus Автор
27.10.2024 17:47И из всего этого комбайна вам требуется... что? Одно единственное значение или вообще идентификатор сессии?
Из всего этого комбайна мне требуется хранилище пар key-value (значения, в основном, строковые). Пар этих там может быть довольно много. Базовая часть идентификатора активного сеанса, номера поколений активных сеансов (активный сеанс пока один на сенанс ASP.NET, но внутри заложена и скоро будет реализована возможность нескольких, с суффиксами в идентификаторах в зависимости от параметров запроса (из коробки - по соответствию пути регулярке) ), хост, на котором выполняется исполнитель (пока - чисто для контроля, правильности настройки sticky session), а исполнителей может быть много. Есть (и с самого начала были) планы сделать всё это распределенным, и там пригодился бы распределенный характер хранилища сеанса, но пока что это только планы.
Некоторые операции (скажем, загрузка файла с регулярными отчётами о прогрессе) актуальны только в том окне, где были инициированы, и весь пользовательский сеанс для них - слишком много.
В активном сеансе может выполняться несколько независимых исполнителей , по одному на операцию. Операция кончилась - исполнитель завершился. Опять-таки, можно прекратить активный сеанс целиком. Разные группы путей запроса - можно завести несколько разных активных сеансов (это скоро будет). Ну и таймауты у активных сеансов и у исполнителей свои, их можно сделать меньше, чем таймаут сеанса ASP.NET, чтобы зря память и процессор не ели.
Другие операции (скажем, синхронизация с другой системой) актуальны для инициировавшего их пользователя или даже для группы пользователей, и пользовательский сеанс для них - слишком мало.
Такие операции - они просто вне области применимости этой библиотеки: она не претендует на роль универсального решения, пригодного для реализации фоновых операций в интересах всего приложения.
mayorovp
27.10.2024 17:47из коробки - по соответствию пути регулярке
А можно без регулярок всё-таки? Ну есть же понятие конечной точки и её метаданных, нужно лишь разобраться как с ними работать...
В активном сеансе может выполняться несколько независимых исполнителей , по одному на операцию.
Ага, и разные окна имеют (потенциальный) доступ к ним всем, независимо от того где кто был запущен. А это - потенциальный источник багов.
И при чём тут вообще сеанс?
mvv-rus Автор
27.10.2024 17:47А можно без регулярок всё-таки? Ну есть же понятие конечной точки и её метаданных, нужно лишь разобраться как с ними работать...
Можно. В основе там - предикат, принимающий HttpContext (он есть уже сейчас, просто это прошло мимо обеих статей). Как с метаданными работать, я знаю. Но надо подумать, как задавать фильтр при конфигурировании (в стиле UseEndpoints - не хочется). Благодарю за идею.
А это - потенциальный источник багов.
Всё, что опирается на куки, будет ровно таким же источником багов.
И при чём тут вообще сеанс?
ASP.NET - хранилище пар ключ/значение
IActiveSession - контейнер сервисов, прежде всего, чтобы брать оттуда Scoped-сервисы. Ну, и ещё кое-что, по мелочи.mayorovp
27.10.2024 17:47Но надо подумать, как задавать фильтр при конфигурировании (в стиле UseEndpoints - не хочется).
Атрибутами на контроллерах и на действиях, как ещё?
Всё, что опирается на куки, будет ровно таким же источником багов.
Ну да, вот потому и нужны альтернативные способы учёта задач. Например, идентификатор можно передавать в заголовке HTTP. Или вовсе включить как часть маршрута.
ASP.NET - хранилище пар ключ/значение IActiveSession - контейнер сервисов [...]
Вот отсюда и вопрос. С фига ли они вообще связаны друг с другом?
mvv-rus Автор
27.10.2024 17:47Атрибутами на контроллерах и на действиях, как ещё?
А как быть с Minimal API? Короче, подумать тут действительно надо.
Ну да, вот потому и нужны альтернативные способы учёта задач. Например, идентификатор можно передавать в заголовке HTTP. Или вовсе включить как часть маршрута.
Не нравится мне эта идея. При использовании куки есть возможность спрятать идентификатор сеанса, так, чтобы его скрипт гарантированно не видел, но повторный запрос к тому же самому сайту его передал.
Ну, а ещё вот лично я привык к возможности работать с одним сайтом из нескольких окон сразу. На базе куки несколько окон в едином сеансе получаются автоматически.Ну, а исполнитель (т.е. задачу) вполне можно привязать и к конкретному окну: в проекте с примерами так и сделано.
Вот отсюда и вопрос. С фига ли они вообще связаны друг с другом?
Так было проще сделать изначально: механизм сеансов ASP.NET Core - он стандартный для привязки запроса к сеансу, уже есть, ничего колхозить не надо. Опять же, схему с несколькими исполнителями в единой сессии (например, по одному на окно) с этим механизмом делать удобно - а мне такая схема работы с сайтами, как я писал выше, нравится.И да, я забыл в прошлый раз упомянтуть, что активный сеанс - это ещё и точка доступа к исполнителям, которые непосредственно выполняют операцию. И эти исполнители благодаря общему активному сеансу могут иметь общие данные и объекты, через которые они могут получать информацию друг о друге (в проекте с примерами есть пример как это делается) и взаимодействовать друг с другом.
В целом, таково проектное решение в данной библиотеке. Минусы у него есть, но и плюсы - тоже. Можно сделать и по-другому, но это, наверное, будет уже другая библиотека.
mayorovp
27.10.2024 17:47Ну, а исполнитель (т.е. задачу) вполне можно привязать и к конкретному окну: в проекте с примерами так и сделано.
Осталось понять, зачем этой задаче/исполнителю нужен остальной сеанс.
mvv-rus Автор
27.10.2024 17:47В этой библиотеке? Здесь как минимум, запрос из браузера привязывается к сеансу, а работающий исполнитель уже ищется через него. Это попадает под категорию "нужен". А ещё можно поинтересоваться "чем полезен": может хранить общие обхекты
В принципе? Можно обойтись. К примеру, автор той статьи (май 2022, ссылка в моей статье есть), из которой я взял исходную идею, вообще вместо сеанса держал в кэше ссылку контейнер сервисов (это не совсем правильно - штатно держать полагается ISessionScope, и очищать(Dispose) его, но не суть). И сеанса как такогого у него изначально не было, а задачей служил Scoped сервис из этого контейнера. Правда, потом, уже после публикации статьи, он наткнулся, видимо, на параллельный доступ к чему-то типа DbContext, которому от параллельного доступа может поплохеть, и добавил сессию, которой кроме контейнера была ещё и блокировка - чтобы исключить параллельный доступ к такому сервису из обработчиков разных запросов.
Ну, или тут первый комментатор разъяснял, как задача запускается из планировщика.Обобщая: в принцепе - можно обойтись, но - так сделано, и из этого можно извлечь даже пользу. Проблему поиметь, правда, тоже можно.
Непонятно, почему вы так против ISession? Я понимаю, что это не стильно, не модно и не молодёжно - т.е., что MS продвигает дргие решения, вроде SignalR и Blazor, но я не понимаю, зачем обязательно следовать моде? Особенно - в вещах некоммерческих, вроде этой библиотеки?
mayorovp
27.10.2024 17:47В этой библиотеке? Здесь как минимум, запрос из браузера привязывается к сеансу, а работающий исполнитель уже ищется через него. Это попадает под категорию "нужен".
Нет, это попадает под категорию "зачем?"
Непонятно, почему вы так против ISession? Я понимаю, что это не стильно, не модно и не молодёжно - т.е., что MS продвигает дргие решения, вроде SignalR и Blazor, но я не понимаю, зачем обязательно следовать моде? Особенно - в вещах некоммерческих, вроде этой библиотеки?
Потому что вы этой привязкой сужаете область применимости своей библиотеки до пустой.
mvv-rus Автор
27.10.2024 17:47Нет, это попадает под категорию "зачем?"
Считаете, что я не в курсе альтернатив? Это неверно.
Считаете, что делать так не надо? Я вас услышал.
Считаете, что нужно все делать единственно правильным образом? Я не согласен - альтернативы стоит, как минимум, проверять.Потому что вы этой привязкой сужаете область применимости своей библиотеки до пустой.
Я ознакомился с вашим мнением, но ему не доверяю: "я в своей практике ещё ни разу не встречал" - слабое основание для столь категорических выводов. Вряд ли вы можете знать всё про применимость в столь широкой области как программирование для веб.
К примеру, я тут не так давно по всяким своим делам рылся в OWASP'овских рекомендациях по защите от CSRF, и случайно обнаружил, что некогда, во времена WebForms, упомянутые сеансы служили штатным способом защиты от этой самой межсайтовой подделки запросов. Ну да, это уже устарело, современные средства хранят и передают защитный маркер по-другому. Но если бы кто-то работал с WebForms с аутентификацией по кукам и был бы при этом озабочен защитой от CSRF, то он, в отличие от вас, в своей практике столкнулся бы с необходимостью использования сеансов ASP.NET.
Поэтому я считаю вашу категоричность необоснованной.И, как я писал в самой статье, применимость библиотеки меня беспокоит мало. Денег за нее я получить не собираюсь, так что могу протестировать с ее помощью и немодное решение. Ну, а быть мейнстримом она все равно не предназначена: я отлично понимаю, что раскрутить ее я не смогу.
Dhwtj
27.10.2024 17:47пока живо сетевое соединение это не особо отличается от асинхронных запросов
так?
а вот если с разных устройств или сессий по одному user id можно получить результаты длительного запроса то да, интересно
mvv-rus Автор
27.10.2024 17:47С разных - нельзя: привязка идет по сессии.
Но держать при этом живое сетевое соединение с одного устройства постоянно не обязательно. Можно интересоваться прогрессом весьма изредка, главное - не слишком редко, чтобы сессия по таймауту не отвалилась.
nronnie
Попробуйте FakeItEasy. Я долгое время использовал Moq, но попробовал его и мне он очень понравился - с тех пор все тесты только на нем. Код с ним намного лаконичнее.
Поставил вам в проект звезду за ваши старания, но, если честно, вот не знаю - есть ли какой-то резон использовать вашу библиотеку вместо веками проверенных решений, например, с Quartz.Net, или Hangfire, или даже любого Message Broker (например, в случае, если он и так уже в проекте используется для чего-либо).
mvv-rus Автор
Нет никакого - она для этого даже не предназначена. Она - не планировщик.
PS А к Moq у меня претензия была, в основном, к документации. Но сейчас я уже претерпелся.
nronnie
Я имел в виду другое. И Quartz.NET, и Hangfire очень часто используют именно не как планировщики, а как менеджеры фоновых задач. Потому что любую фоновую задачу можно легко рассматривать как задачу планировщика, только не поставленную на какой-либо расписание, а запущенную сразу. И API и того и другого напрямую это поддерживает (запуск любой задачи сразу без расписания).
mvv-rus Автор
Теперь понял, что вы имеете в виду.
В таком случае у меня возникает пара вопросов по ним, не ответите на них?
Во-первых, удобно ли с ними делать привязку к сессии пользователя в браузере?
Во-вторых, легко ли сделать так, чтобы они вернули промежуточные результаты задачи, пока она выполняется?
Если да, то скорее всего, без описываемой библиотеки обойтись достаточно легко.
А насчет использовать... Я отлично понимаю, что эта библиотека как решение - весьма нишевое, нужное реально немногим: индустрия сейчас пошла по другому пути. Но меня это не смущает - писал я ее чисто для своих целей, можно сказать - для своего удовольствия.
PS За звезду благодарю.
nronnie
Я так не очень понял, но попробую угадать. Если мы хотим чтобы клиент сам опрашивал нашу фоновую задачу, то после её создания (HTTP POST от клиента) возвращаем на клиент её ID (в Quartz у каждой задачи есть свой уникальный ID), потом клиент через HTTP GET с ID этой задачи может через API Quartz получать о ней информацию. Если хотим оповещать клиента по SignalR, то при создании задачи вставляем в неё с DI компонент, через который она (задача) уже когда надо сама отправляет клиенту сообщения. Это для случая Quartz. А Hangfire я не люблю и крайний раз имел с ним дело очень и очень давно, но там как-то так тоже всё похоже. Если я не так понял, то просто опишите словами какой-нибудь сценарий, а я тогда уже точно скажу как его можно реализовать.
mvv-rus Автор
В целом, вы поняли правильно. Но есть нюансы.
Могу показать код - у меня есть дистрибутив с примерами (ссылка в статье). Но, наверное, это неправильно заставлять человека скачивать код, которым он вряд ли будет пользоваться, поэтому попробую на словах.
Есть страничка, с которой пользователь может отслеживать (опросом) количество неких запущенных им (именно им) фоновых задач (в примере это - задачи, запущенные со страничек других примеров). А также - следить за измененим их числа - запускать триггер, который сработает, когда количество изменений дойдет до указнного (триггер в примере для простоты реализуется на тупом long poll). Вопрос первый - как это реализовать Quartz.net (допустим, вспомогательные классы из примера уже есть)? Вопрос второй - будет ли такая реализация удобной?
PS Если что, то это вот этот пример
PPS Хотя, наверное, из словесного описания непонятно, но эта задача сама никогда не заканчвается (ну, если ее не прекратить извне, закрытие странички прекращает).
nronnie
Ну, вот, пример (с Quartz) - создание задач, получение списка id задач, остановка задачи по id. Нет только контроля за их количеством, но это легко и уже из кода понятно как можно сделать. Код настолько простой, что я даже Visual Studio не запускал - набивал всё в Code.
Program.cs
Полностью (с
*.csproj
и прочим) можно посмотреть здесь.mvv-rus Автор
Да, рановато мне джиннам желания загадывать... :-)
Ваш код соответствует тому, что я там написал (почти - есть нюанс, о нем ниже), но в виду я имел совсем другое:
задачи, количество, которых надо отслеживать, вполне себе конечные - они имитируют выборку записей из последовательностей (
IEnumerable<T>
илиIAsyncEnumerable<T>
), которые перечисляются с задержками (может быть несколько стадий с разными задержками, в т.ч. с нулевой) и выводят их в несколько приемов на страничку. Впрочем прервать их до срока тоже можно (кнопка даже на страничке есть, ну, или страничку закрыть).бесконечная задача - это та фоновая задача, та, которая за ними наблюдает. Вот она завершится только если ее страничку закрыть.
функциональность этой задачи - запуск некоего фонового процесса, работу которому пришлось придумывать. Вот я и придумал - учет числа выполняющихся задач. Ее пришлось делать самому: библиотека - не планировщик, в ней этой функциональности нет. А в планировщике такая функциональность, естественно, есть, так что у вас потому просто и получилось, те вспомогательные классы из моего примера вам не понадобились.
А вот чего не увидел (тот самый нюанс) - реакции на завершение задачи, чтобы вернуть этот факт скрипту на страничку через long poll вызов API. Подозреваю, что у планировщика нужная функциональность должна быть, так что это, наверняка, делается. Ну, и ещё, в моем примере возвращался не список задач, а их число, но это исправить элементарно.
Собственно, тот пример был - он не на учет и контроль других задач, а на запуск произвольного процесса в фоне и получение результатов от него в промежутке. Чтобы было не совсем уныло, в качестве такого процесса был выбран учет и контроль выполняющихся задач(в библиотеке они именуются, кстати, исполнителями) определенного типа.
Что нет в функциональности вашего примера с планировщиком - встроенной изоляции одного сеанса от другого, и придется такие вещи доверять скрипту фронта (и проверять, естественно - с учетом возможности CSRF). В обсуждаемой библиотеке это есть сразу за счет интеграции с ISession (котрая работает через недоступную скрипту фронта куку).
Впрочем, я изначально понимал, что так или иначе функции обсуждаемой библиотеки можно при желании выполнить и другим способом. Но если осталось желание продолжить, можно обсудить и другие примеры.
posledam
Вот какие задачи мы решали с помощью SignalR без написания каких-то библиотек, с минимум кода:
Таймер времени на сессию пользователя, который ведётся на сервере. В одном проекте у каждого пользователя есть KPI, который считает сколько он работы проделал за время сессии, и ещё этапы работы автоматически закрываются по периодам, либо по количеству работы, и открываются новые. Всё делается автоматически в контексте сессии пользователя.
Запуск длительных обработок, например, построение отчёта. Пользователь запускает операцию, и потом продолжает работать в системе, при этом пользователю отображается прогресс(ы) запущенных операций во время его работы в системе. По завершению любой запущенной операции он может вернуться к ней и посмотреть результат или пошарить другим юзерам.
Похоже на ваши потребности?
mvv-rus Автор
На мои потребности - нет: они - вообще вне контекста какой-либо инженерной задачи.
На возможности библиотеки - похоже.
Но я, вроде как, нигде и не утверждал, что она будет единственно возможным или хотя бы наилучшим решением.
VanKrock
на самом деле сюда SignalR хорошо ложится, не понятно зачем тут серверные сессии. Можно создать Singleton сервис в нём ConcurrentDictionary с активными задачами, внутри задачи отсылаете промежуточные статусы по SignalR в после выполнения задача удаляет себя из списка активных задач. При таком подходе вам не нужно с фронта опрашивать бэк, он сам будет всё присылать, нужно только подписаться на нужное. Так же не обязательно, чтобы промежуточные результаты по задачам прилетали только пользователю который их запустил
mvv-rus Автор
Вы имеете в виду дргугой сценарий использования, не тот, для которого предназначена библиотека ActiveSession. Она как раз предназначена, чтобы результаты фоновой операции приходили иеннно тому пользователю, который ее запустил: это его личная операция, других она не касается. Зато она не ограничивается сценариями реального времени, для которых предназначен SIgnalR: дальнейший результат можно запрашивать даже просто переходом по ссылке. И получать в ответ хоть статическую страницу совсем без скриптов - а SIgnalR нужен активный скриптовый клиент, поддерживающий постоянное соединение с сервером.
Короче, ActiveSession ориентирована на другой сценарий использования. Насколько такой сценарий использования кому-либо нужен - это другой вопрос. И я в курсе, что этот сценарий далек от магистрального пути, по которому пошел интернет, про это даже в статье есть, в заключительной части.