Перейти к содержанию

Сервис Chronos

Chronos – это приложение, позволяющее периодически запускать фоновые сервисы (плагины) как сервис Windows/Linux или как приложение в окне консоли (для отладки). Chronos позволяет очень просто реализовать любую фоновую работу или работу по расписанию, которая обычно выполняется написанием и установкой отдельных сервисов Windows/Linux. Например, плагин Chronos может пересчитывать динамические роли или удалять карточки из корзины, которые лежат там более 6 месяцев.

Хост Chronos

Запускается как консольное приложение или устанавливается как сервис Windows или Linux.

  • Установка и запуск сервиса (запустите в командной строке от имени администратора):

    install-and-start.bat

  • Установка без запуска сервиса:

    install.bat

  • Остановка и удаление сервиса без удаления:

    uninstall.bat

Хост выполняет планирование и запуск найденных плагинов в соответствии с расписанием, которое задают сами плагины через атрибуты или конфиги.

Хост ищет плагины внутри задаваемой в конфиге хоста папки Plugins или в её подпапках на 1 уровень вниз. Подпапка плагина должна содержать файл plugins.xml (см. ниже), перечисленные в нём файлы .DLL для плагинов и все их зависимости.

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

В оснастке ‘Службы’ сервис называется Syntellect Chronos

Содержимое дерева с плагинами (папки в квадратных скобках)

| |_ app.json |_ Chronos.exe |_ Chronos.dll |_ Chronos.Contracts.dll |_ Chronos.Platform.dll |_ Chronos.Platform.Linux.dll |_ NLog.dll |_ NLog.config |_ ... | |_ [ extensions ] | | | |_ extensions.xml | |_ Tessa.Extensions.Default.Server.dll | |_ Tessa.Extensions.Default.Shared.dll | |_ Tessa.Extensions.Server.dll | |_ Tessa.Extensions.Shared.dll | |_ [ Plugins ] | |_ [ MyPlugin1Folder ] | | | |_ app.json | |_ Chronos.Contracts.dll | |_ MyPlugin1Assembly.dll | |_ MyPlugin1Config.xml | |_ plugins.xml | |_ extensions.xml | |_ [ MyPlugin2Folder ] | | | |_ app.json | |_ Chronos.Contracts.dll | |_ MyPlugin2Assembly.dll | |_ NLog.dll | |_ Tessa.dll | |_ extensions.xml | |_ Chronos.Contracts.dll |_ MyPlugin3Assembly.dll |_ MyPlugin4Assembly.dll |_ Some.Useful.Shared.Lib.dll |_ plugins.xml |_ extensions.xml

Хост запускает каждый плагин в отдельном процессе-обёртке и логирует необработанные исключения.

При вежливой остановке хоста процессы всех плагинов будут гарантированно завершены. При аварийной остановке хоста (при закрытии окна консоли или при завершении процесса хоста через диспетчер задач) все процессы плагинов завершаются с вероятностью 99,9%. Если процессы плагинов не удалось завершить, то они будут гарантированно завершены при повторном запуске хоста.

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

Пример файла plugins.xml

<?xml version="1.0" encoding="utf-8" ?> <plugins xmlns="http://syntellect.ru/chronos/include"> <include file="Tessa.Chronos.dll" /> </plugins>

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

Хост использует планировщик link:http://quartznet.sourceforge.net[Quartz.NET]. Он поддерживает Cron, пул потоков, довольно гибок в настройке, масштабируется и тестировался под нагрузкой в Enterprise-решениях. Лицензия Apache 2.0.

Файл app.json в каждой папке позволяет задать конфигурационные параметры плагинов.

При использовании API с расширениями требуется указать файл extensions.xml, в котором задан список сборок с расширениями (extensions.xml могут ссылаться друг на друга, как пример extensions.xml в папке Tessa типовой сборки ссылается на файл extensions.xml в папке extensions).

Пример файла extensions.xml

<?xml version="1.0" encoding="utf-8" ?> <extensions xmlns="http://syntellect.ru/tessa/include">

<include file="Tessa.Extensions.Default.Server.dll" serverOnly="true" /> <include file="Tessa.Extensions.Default.Shared.dll" />

<include file="Tessa.Extensions.Server.dll" serverOnly="true" /> <include file="Tessa.Extensions.Shared.dll" />

</extensions>

Дополнительные параметры командной строки

  • Справка по параметрам командной строки с указанием версии Chronos:

    Chronos.exe --help

  • Запуск в режиме сервиса Windows (используется в командных файлах, не требуется вызывать вручную):

    Chronos.exe --service

Настройки Chronos в app.json

  • PluginFolderName - имя папки относительно папки хоста, внутри которой или внутри подпапок которой располагаются плагины. Пример: ‘Plugins’.

  • AwaitGracefulStopSeconds - время в секундах (вещественное число), которое хост ожидает, пока все плагины не завершат свою работу. Если время вышло, то процессы всех плагинов будут принудительно завершены. Пример: ‘1.5’.

  • AwaitCancellationDeltaSeconds - количество секунд до завершения периода AwaitGracefulStopSeconds, за которое будет автоматически запущена остановка через cancellationToken, передаваемый в метод EntryPointAsync каждого плагина. При этом обычно прерываются все текущие активности плагина, но плагин может выполнить очистку (блоки finally, логирование и др.). Например, если AwaitGracefulStopSeconds указан как 30 секунд, а AwaitCancellationDeltaSeconds как 2 секунды, то через 28 секунд от начала остановки во всех ещё не завершённых плагинах объект cancellationToken будет в состоянии “отмена”.

  • ChronosSyncTimeout - таймаут ожидания синхронизации между процессами. По умолчанию указано 15 секунд.

  • ProbingPath - список папок для загрузки сборок помимо собственно папки с плагинами; папки разделены точкой с запятой. Пути к папкам указываются относительно папки с хост-процессом (запускающим файлом Chronos.exe).

Класс плагина

Это класс, наследуемый от базового класса Plugin и имеющий атрибут [Plugin].

using Chronos.Contracts

namespace MyPluginNamespace { [Plugin] public sealed class MyPlugin : Plugin { public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { // код плагина TessaPlatform.InitializeFromConfiguration();

IUnityContainer container = await new UnityContainer().RegisterServerForPluginAsync(); // ... } } }

С помощью свойств Name, Description и Version можно опционально указать имя, описание и версию плагина, которые будут использоваться хостом в информационных целях (для вывода в логи или в окно консоли).

[Plugin(Name = "My plugin #2", Description = "This plugin can do a lot of things.", Version = 3)] public sealed class MyPlugin : Plugin { public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { // код плагина } }

При использовании наследования атрибуты плагина не наследуются.

public abstract class MyPluginBase : Plugin { // этот класс не считается плагином, т.к. у него отсутствует атрибут [Plugin]

// если бы он присутствовал, то хост не смог бы его запустить, т.к. класс абстрактный

// если бы класс не был абстрактным, то дочерние классы не являлись бы автоматически плагинами // и при наличии у них атрибута [Plugin] могли бы работать по совершенно другому расписанию

protected abstract void DoWork(string s);

public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { // некоторая начальная инициализация this.DoWork("some string"); } }

[Plugin] public sealed class MyPlugin1 : MyPluginBase { // этот плагин будет считаться самым обыкновенным

public override void DoWork(string s) { // код плагина } }

[Plugin] public sealed class MyPlugin2 : MyPluginBase { // хост будет считать, что MyPlugin2 - это ещё один плагин и он никак не связан с MyPlugin1

public override void DoWork(string s) { // код плагина } }

Особенности плагина и его сборки

  • Сборка .DLL плагина:

    • Должна ссылаться на Chronos.Contracts.dll.

    • Может содержать несколько плагинов.

    • Должна располагаться в определённой папке хоста.

  • Плагин запускается хостом в отдельном процессе.

  • Плагин не содержит интерфейс обратного вызова хоста.

  • Плагин может почти что угодно: например, использовать API, предоставляемое Tessa. Оно позволяет:

    • Выполнить некоторую операцию над БД.

    • Учесть специальные блокировки (например, большинство таблиц справочника ролей не может одновременно изменяться несколькими плагинами). Блокировка реализуется через специальное поле Locked в некоторой таблице в БД.

Хост может завершить процесс плагина при своём завершении или перезапуске. Если плагин не успевает сделать какие-то ломающие изменения (например, в базе), то это его задача: восстановить систему в нормальное состояние при повторном запуске.

Желательно, чтобы всё взаимодействие legacy плагина с базой происходило в транзакциях.

Пример: если процесс установки блокировки, изменения БД и удаления блокировки прервать, то плагин должен начать транзакцию, сделать SELECT WITH ( HOLDLOCK ) или WITH ( XLOCK ) на отдельную таблицу с блокировками, наполнить 3 временные таблицы для INSERT, UPDATE, DELETE, далее вызовом трёх команд модифицировать таблицу и завершить транзакцию.
Примечание: блокировка статического справочника ролей не имеет смысла.

Плагин может писать в лог при помощи NLog, но при этом будут использоваться правила логирования, определённые в NLog.config хоста.

Расписание запуска плагина

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

  • Плагин вызывается один раз при запуске хоста. Такой плагин может работать в вечном цикле, делать какую-то работу и периодически засыпать. Плагин также может использовать любой планировщик, например из библиотеки link:http://quartznet.sourceforge.net/[Quartz.NET]. Однако, нельзя создавать хост плагинов внутри плагина.

    [Plugin] public sealed class OneTimePlugin : Plugin { private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

    public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { // здесь начинается код плагина: он может содержать собственный механизм планирования

    // для кода этого метода гарантируется, что он не будет вызван хостом одновременно в разных процессах

    // при завершении хоста процесс, в котором выполняется плагин, принудительно завершается; // поэтому не следует выполнять в плагине ломающие БД изменения, не заключив их в транзакцию

    for (int i = 0; i < 10; i++) { logger.Trace("Plugin is running..."); await Task.Delay(1000, cancellationToken); }

    logger.Trace("Plugin has been stopped and will not run again until host will be restarted."); } }

  • Плагин вызывается с указанной периодичностью (каждые 10 минут)

    [Plugin(RepeatSecondInterval = 10 * 60, DisallowConcurrency = true)] public sealed class PeriodicPlugin : Plugin { public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { // для кода этого метода гарантируется, что он не будет вызван хостом одновременно в разных процессах, // только если установлено свойство DisallowConcurrency = true атрибута [Plugin]

    // иначе плагин может выполняться слишком долго и за прошедшее время будет вызван ещё один процесс плагина

    // кроме того, ОС может перевести системные часы (например, при автоматической синхронизации времени), // и плагин тут же будет вызван повторно, что приведёт к побочным эффектам, // если плагин использует некоторый внешний ресурс (например, таблицу в БД) } }

  • Интервал вызова плагина определяется выражением Cron. Пример: запускать каждую среду в 12:00 и каждую пятницу в 0:00. Если условие вызова нельзя построить одним выражением Cron, то дополнительные выражения могут задаваться с помощью атрибутов PluginTrigger.

    [Plugin(DisallowConcurrency = true)] [PluginTrigger(Cron = "0 0 12 * * WED")] [PluginTrigger(Cron = "0 0 0 * * FRI")] public sealed class CronPlugin : Plugin { public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { // код плагина } }

  • Расписание вызова плагина загружается из конфигурационного файла. В данном примере файл pluginConfig.xml должен располагаться в одной папке со сборкой плагина.

Tip

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

[Plugin(ConfigFile = "pluginConfig.xml"))] public sealed class ConfigFilePlugin : Plugin { public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { // код плагина } }

Пример содержимого файла pluginConfig.xml:

<?xml version="1.0" encoding="UTF-8"?> <plugin xmlns="http://syntellect.ru/chronos" name="Plugin from config" description="Info for this plugin is loaded from config file." version="5" disallowConcurrency="true" disabled="true">

<!-- disabled="true" определяет, что плагин не будет использоваться (можно временно его отключить через конфиг) -->

<!-- Запускать каждую среду в 12:00 --> <trigger cron="0 0 12 ? * WED" />

<!-- Дополнительно запускать с интервалом в 36 часов. Поскольку начало отсчёта не определено, лучше вместо этого использовать выражение Cron. --> <trigger repeatSeconds="129600" /> </plugin>

Пример файла pluginConfig.xml, не содержащего информацию о плагине (при отсутствии другой информации из атрибутов Plugin и PluginTrigger плагин будет вызван один раз, как будто бы конфигурационного файла не было):

<?xml version="1.0" encoding="UTF-8"?> <plugin xmlns="http://syntellect.ru/chronos" />

В каждом из атрибутов Plugin и PluginTrigger можно указывать не более одного из свойств RepeatSecondPeriod, Cron, ConfigFile. Все другие комбинации атрибутов и их свойств допустимы. Например, частично информацию о плагине можно задать через атрибуты, а другую часть получить из одного или нескольких конфигурационных файлов.

Note

Описание синтаксиса CRON, который используется в расписании плагинов, а также в динамических ролях и генераторах метаролей, можно найти здесь: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/crontrigger.html

Примеры плагинов

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

  • Пересчёт таблиц замещений в статических ролях. Плагин вызывается периодически.

  • ADSync: синхронизация сотрудников и ролей с Active Directory.

    • Плагин вызывается периодически по выражениям Cron (для синхронизации по требованию должен напрямую вызываться соответствующий метод API).

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

Плагин с вежливой остановкой

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

using Chronos.Contracts;

[Plugin] class MyGracefulPlugin : Plugin { public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { /* do work */ await Task.Delay(5000, cancellationToken);

if (this.StopRequested) { ... } }

public override async Task StopAsync(IGracefulStopToken token) { // это поведение по умолчанию в базовом классе, поэтому переопределять // таким образом метод не требуется. но можно его дополнить this.StopRequested = true; return await token.WaitUntilEntryPointFinishedAsync(token.CancellationTokenSource.Token); } }

  • StopAsync - метод, вызываемый хостом внутри процесса плагина при вежливой остановке плагина.

  • StopRequested - флажок, устанавливаемый в методе StopAsync, его может периодически проверять EntryPointAsync.

  • Параметр IGracefulStopToken token - токен, позволяющий определить состояние плагина из метода его вежливой остановки. Его метод WaitUntilEntryPointFinishedAsync дожидается завершения работы метода EntryPointAsync плагина. Свойство EntryPointFinished позволяет проверить, завершён ли уже метод EntryPoint. Свойство CancellationTokenSource содержит объект, позволяющий отменить токен cancellationToken, передаваемый в метод EntryPointAsync. Токен будет отменён автоматически по настройке AwaitCancellationDeltaSeconds в Chronos.dll.config за несколько секунд до того, как процесс плагина будет закрыт хост-процессом по таймауту, поэтому вызывать CancellationTokenSource.Cancel() необязательно.

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

Хост вызовет метод StopAsync в потоке, отличном от потока, в котором выполняется метод EntryPointAsync. Если метод StopAsync неожиданно прервётся, то исключение будет добавлено в лог.

Вежливая остановка может происходить при остановке хоста, запущенного как сервис Windows или Linux, или при вводе команды остановки stop в хосте, запущенном в консоли. При этом все работающие плагины имеют некоторое время, определяемое настройкой AwaitGracefulStopSeconds хоста (порядка 30 секунд), для того, чтобы корректно завершить свою работу. Вежливой остановки не производится при закрытии окна консоли или при завершении процесса хоста через диспетчер.

Пример плагина с вежливой остановкой

using System.Threading; using Chronos.Contracts;

[Plugin(Name = "Graceful plugin", RepeatSecondInterval = 10)] class MyGracefulPlugin : Plugin { public override async Task EntryPointAsync(CancellationToken cancellationToken = default) { while (!this.StopRequested) { await Task.Delay(100, cancellationToken); // do work } } }

Сборка плагинов для проектного решения

В расширениях проектного решения (папка Source в сборке) присутствует проект Tessa.Extensions.Chronos, в котором рекомендуется располагать любые плагины (фоновые сервиса), используемые в вашем проекте.

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

  1. Убедитесь, что сервис Chronos остановлен.

  2. В папке сервиса Chronos скопируйте содержимое папки Plugins\Tessa в новую папку Plugins\Tessa.Extensions.Chronos

  3. Удалите подпапку Plugins\Tessa.Extensions.Chronos\configuration.

  4. Удалите следующие файлы в папке Plugins\Tessa.Extensions.Chronos: Tessa.Chronos.dll, Tessa.Extensions.Default.Chronos.dll, unoconv.

  5. Убедитесь, что в xml-файлах плагина, расположенных в папке Extensions\Tessa.Extensions.Chronos\configuration, требуемые плагины включены, т.е. у них указано disabled=”false”. В плагине-примере ExamplePlugin по умолчанию указано disable=”true”, замените его на “false”.

  6. Соберите проект Tessa.Extensions.Chronos в Visual Studio.

  7. После сборки появится папка Bin\Tessa.Extensions.Chronos (относительно папки с файлом .sln), скопируйте её содержимое с заменой в Plugins\Tessa.Extensions.Chronos.

  8. Запустите сервис Chronos в окне консоли. Названия ваших плагинов в сборке Tessa.Extensions.Chronos должны быть выведены на экране.

По умолчанию это ExamplePlugin, который также выполняет запись в log.txt на уровне Trace. Вы можете включить этот уровень логирования в NLog.config в папке с Chronos, заменив строку: <logger name=”*” minlevel=”Trace” writeTo=”file” />

В дальнейшем для обновления плагина достаточно шагов:

  1. Остановите сервис Chronos.

  2. Соберите проект Tessa.Extensions.Chronos в Visual Studio.

  3. Скопируйте содержимое папки Bin\Tessa.Extensions.Chronos с заменой в Plugins\Tessa.Extensions.Chronos.

  4. Запустите сервис Chronos.

Копирование можно автоматизировать при сборке, записав скрипт копирования в Extensions\Tessa.Extensions.Chronos\post-build.bat.

Инструкция также доступна в файле readme.txt в папке проекта Tessa.Extensions.Chronos.

Использование конфигурации из файла app.json

Chronos позволяет хранить настройки для плагинов в файле app.json.

Например, нам необходимы несколько настроек для плагина TestPlugin: TestString - строковое значение, TestInt - числовое значение.

Для этого необходимо добавить в блок Settings файла app.json:

Warning

Внимательно следите за синтаксисом! Json требует экранирования ‘': вместо ‘' необходимо указывать ‘\‘. Так же требуются запятые при перечислении элементов настроек, если синтаксис будет некорректным, то это приведет к ошибке при запуске любых плагинов Chronos.

{ "Settings": { "TestPlugin.TestString": "test", "TestPlugin.TestInt": 1 } }

Получить эти настройки изнутри плагина можно так:

logger.Info("TestString = '{0}'", ConfigurationManager.Settings.TryGet("TestPlugin.TestString", string.Empty)); logger.Info("TestInt = '{0}'", ConfigurationManager.Settings.TryGet("TestPlugin.TestInt", 0));

Параметры логирования NLog для плагина

Note

Информация из этого раздела актуальна только для Chronos версии 1.3 или более поздней.

Если плагину требуются отличные от хоста параметры логирования, и он использует NLog, то определить уникальные для сборки с плагином параметры можно в её файле NLog.config, который следует расположить в одной папке со сборкой плагина. В качестве {basedir} будет использоваться папка с Chronos.exe, а не папка, в которой лежит NLog.config.

Пример файла NLog.config с конфигурацией NLog:

<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<targets async="true"> <target name="file" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}\Plugins\MyPlugins\log.txt" /> <target name="queries" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}/queries.txt" layout="--${longdate}${newline}${message}${newline}GO${newline}" /> <target name="process" xsi:type="File" encoding="utf-8" writeBom="true" fileName="${basedir}/process.txt" layout="${longdate}${newline}${message}${newline}" /> <target name="null" xsi:type="Null" formatMessage="false" /> </targets>

<rules> <logger name="SqlQueries" minlevel="Error" writeTo="queries" final="true" /> <logger name="SqlQueries" minlevel="Trace" writeTo="null" final="true" /> <logger name="Process" minlevel="Error" writeTo="process" final="true" /> <logger name="Process" minlevel="Trace" writeTo="null" final="true" /> <logger name="Configuration" minlevel="Info" writeTo="file" final="true" /> <logger name="Configuration" minlevel="Trace" writeTo="null" final="true" /> <logger name="Quartz.*" minlevel="Error" writeTo="file" final="true" /> <logger name="Quartz.*" minlevel="Trace" writeTo="null" final="true" /> <logger name="*" minlevel="Info" writeTo="file" /> </rules>

</nlog>

Back to top