Ну вот, я наконец-то сделал ее — первую версию библиотеки 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.
ссылка на оригинал статьи https://habr.com/ru/articles/850940/
Добавить комментарий