# Техническое устройство и разработка

Страница объясняет внутреннее устройство менеджера заданий и поддерживаемые способы его использования в прикладном коде. Пользовательский интерфейс описан на странице [«Интерфейс менеджера заданий»](020_interface.md), настройка и диагностика — на странице [«Администрирование и диагностика»](040_administration.md), механизм очистки — на странице [«Очистка данных»](030_cleanup.md).

Прикладной разработчик должен работать через контракты `*Apic`. Низкоуровневые классы Quartz и внутренние реализации `*Api` используются внутри менеджера заданий и не являются основным межмодульным контрактом.

## Архитектура

Менеджер заданий состоит из прикладной модели заданий, Quartz с реализациями `GsfJobStore` и `GsfThreadPool`, отдельного процесса `jobscheduler` и ApplicationServer.

<!-- Начало кода -->
```text
Менеджер заданий
├── Btk_Job (конфигурация задания и общие ограничения)
├── Btk_JobSchedule (расписания задания)
├── Quartz JobStore (технические задания и триггеры)
├── GsfJobStore (регистрация времени получения триггеров)
├── GsfThreadPool (пулы выполнения и очереди конкурентных запусков)
├── jobscheduler (определяет наступившие запуски)
├── ApplicationServer (выполняет прикладную операцию)
├── Btk_JobEvent (состояние и результат попытки выполнения)
└── Btk_JobEventLog и Btk_JobStats (сообщения и статистика)
```
<!-- Конец кода -->

Quartz 2.3.1 использует `GsfJobStore` поверх PostgreSQL с префиксом таблиц `btk_qrtz_`, именем экземпляра `PostgresScheduler` и делегатом PostgreSQL. Потоки выполнения предоставляет `GsfThreadPool`. Точная версия зависимости `ru.bitec:jobscheduler` определяется поставкой.

Схема показывает основные внутренние компоненты `jobscheduler`, Quartz и пула потоков. Она нужна для понимания взаимодействия планировщика с JobStore и потоками выполнения; прикладной код не должен обращаться к этим компонентам напрямую.

![Диаграмма компонентов Quartz Scheduler](img/quartz_scheduler_class_diagram.png)

## Планировщик и Quartz

`Btk_Job` не является Quartz job. Запись задания хранит прикладную конфигурацию, а техническая запись Quartz создается из нее при синхронизации.

При изменении задания или расписания система устанавливает `bSync = 0`. После сохранения изменений `updateJob` обрабатывает несинхронизированные задания пакетами:

1. Для выключенного задания удаляет техническое представление из Quartz.
2. Для включенного задания создает или заменяет Quartz job.
3. Для активных расписаний создает `SimpleTrigger` или `CronTrigger`.
4. Для выключенных и удаленных расписаний удаляет триггеры.
5. После обработки устанавливает `bSync = 1`.

Данные Quartz job содержат адрес SOAP-точки, идентификатор базы данных, данные аутентификации, пользователя выполнения, JEXL-вызов менеджера заданий и параметры конкурентности. `SoapReqJob` и `SoapReqJobConcurrent` служат внутренними Quartz-адаптерами: преобразуют срабатывание триггера в запрос выполнения к ApplicationServer. Прикладной код не выбирает эти классы напрямую.

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

Таблицы Quartz являются производными. Изменять их напрямую из прикладного модуля нельзя. Полная пересинхронизация и ее последствия описаны на странице [«Администрирование и диагностика»](040_administration.md#принудительная-синхронизация).

### Цикл выполнения Quartz

При запуске процесса `jobscheduler` `SchedulerHelper` создает и инициализирует `SchedulerManager`. Менеджер создает `Scheduler`, а тот — `QuartzScheduler` и `QuartzSchedulerThread`. После запуска Quartz поток планировщика начинает обрабатывать триггеры.

Один цикл обработки планового запуска проходит следующие этапы:

1. `QuartzSchedulerThread` через `JobStore.acquireNextTriggers` получает триггеры в состоянии `WAITING`, для которых наступило расчетное время в допустимом окне. Полученные триггеры переходят в состояние `ACQUIRED`.
2. `JobStore.triggersFired` фиксирует срабатывание триггеров. Для одноразового триггера без следующего времени устанавливается состояние `COMPLETE`. Для непараллельного задания остальные триггеры этого задания блокируются (`BLOCKED`), а для параллельного задания следующий запуск остается в `WAITING`.
3. Для выбранных запусков создаются выполняемые оболочки, которые передаются в `GsfThreadPool`. Запись активного срабатывания триггера получает состояние `EXECUTING`.
4. Оболочка вызывает SOAP-операцию `executeJexl` ApplicationServer. Дальнейшее выполнение прикладной операции и сохранение `Btk_JobEvent` описаны в разделе [«Плановое выполнение»](#плановое-выполнение).
5. После завершения оболочка вызывает `JobStore.triggeredJobComplete`. Quartz удаляет запись активного срабатывания и либо рассчитывает следующее время запуска и возвращает триггер в `WAITING`, либо удаляет завершенный одноразовый триггер.

Состояние Quartz-триггера и состояние события `Btk_JobEvent` описывают разные уровни выполнения. Например, `ACQUIRED` означает получение триггера планировщиком, а «Выполняется» — наличие выполняющейся серверной сессии задания.

### Хранение триггеров и блокировки

Quartz хранит технические задания и триггеры в PostgreSQL в таблицах с префиксом `btk_qrtz_`. Эти таблицы являются производным представлением конфигурации менеджера заданий и заполняются при синхронизации `Btk_Job` и `Btk_JobSchedule`.

Для работы с PostgreSQL используется `GsfJobStore`, основанный на стандартном Quartz JobStore. Его дополнительная функция в системе — регистрация времени получения триггеров в `SchedulerStatMonitor`. Распределение запусков по потокам и очередям выполняет `GsfThreadPool`.

Право активного выполнения заданий между несколькими процессами `jobscheduler` координируется одним из двух режимов:

- REST-блокировка ApplicationServer. Планировщик периодически обращается к сервису блокировки. Процесс, получивший блокировку, запускает Quartz. Остальные процессы переводят Quartz в режим ожидания (`standby`).
- PostgreSQL advisory lock. В режиме высокой доступности несколько процессов могут быть запущены одновременно, но выполнять задания может только процесс, которому удалось получить advisory lock в PostgreSQL. При потере блокировки текущий Quartz останавливается, после чего процесс пытается создать его заново.

Конкретный режим определяется конфигурацией поставки. Несколько процессов повышают отказоустойчивость, но не позволяют нескольким master-процессам одновременно выполнять одну и ту же очередь.

Блокировка планировщика координирует право на получение и выполнение Quartz-запусков. Она не заменяет транзакции, блокировки и проверки полномочий прикладного кода, выполняемого в ApplicationServer.

### Потоки и конкурентность

`bpConcurrent = true` разрешает параллельные экземпляры одного задания. `nUnscheduledThreads` задает лимит внеплановых потоков для этого задания, а `Btk_JobConcurrentThreadSchedule` позволяет менять лимит по дню недели и часу. Рассчитанное значение передается планировщику как `CONCURRENT_THREADS_NUM`.

`GsfThreadPool` выполняет обычные Quartz job в основном пуле. Для конкурентных заданий он создает отдельные пулы, поэтому лимит одного задания не расходуется другими конкурентными заданиями. Увеличение `nUnscheduledThreads` повышает параллелизм только соответствующего задания и одновременно увеличивает возможное число выполняющих потоков процесса.

`GsfThreadPool` распределяет конкурентные запуски по группам очереди, заданным в `spGroup`. Между непустыми группами действует алгоритм round-robin, внутри каждой группы — FIFO. Round-robin не гарантирует одинакового времени выполнения заданий. Очереди находятся в памяти процесса, поэтому после перезапуска их прежний порядок не сохраняется.

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

### Запуск процесса jobscheduler

`jobscheduler` запускается отдельно от ApplicationServer. Он поддерживает CLI-режимы `-start`, `-startOnPort` и `-stopOnPort`, а также запуск как Linux- или Windows-служба. Конкретный unit-файл и команда запуска зависят от эксплуатационной поставки. Во всех вариантах процессу нужны конфигурация Quartz, подключение к PostgreSQL, адрес ApplicationServer и данные системной аутентификации.

Несколько процессов могут работать с общим JobStore только при общей настройке координации через REST-lock или PostgreSQL advisory lock. Само добавление процессов не увеличивает лимиты отдельных конкурентных заданий: их определяют параметры, переданные в `GsfThreadPool`. Нельзя одновременно смешивать разные механизмы блокировки для экземпляров, обслуживающих один JobStore.

### Служебные компоненты процесса

Процесс `jobscheduler` запускает служебные компоненты, которые предотвращают дублирование запусков и зависание занятых потоков, собирают сведения о состоянии процесса и предоставляют их для диагностики:

При запуске `jobscheduler` компонент `SchedulerManager` создает `SchedulerStatMonitor`, запускает `JobThreadWatcher` и `GsfWebServer`. В обычном режиме он также запускает `SchedulerLocker`, который управляет состоянием Quartz через REST-блокировку. В режиме высокой доступности PostgreSQL advisory lock получает и контролирует сам `SchedulerManager`.

| Компонент | Назначение |
|---|---|
| `JobThreadWatcher` | Сопоставляет выполняемые Quartz-запуски с активными рабочими сессиями ApplicationServer и прерывает Quartz job при устойчивом расхождении |
| `SchedulerStatMonitor` | Собирает оперативные показатели текущего процесса планировщика |
| `SchedulerLocker` | Управляет запуском и режимом ожидания Quartz при координации через REST-блокировку ApplicationServer |
| `GsfWebServer` | Публикует состояние планировщика в диагностическом HTML-интерфейсе и REST-сервисе |

Компоненты используют общий экземпляр Quartz и клиент ApplicationServer, но решают независимые задачи. `GsfJobStore` и SOAP-задания передают оперативные данные в `SchedulerStatMonitor`, а `GsfWebServer` публикует собранное состояние. `JobThreadWatcher` и `SchedulerLocker` обращаются к Quartz и ApplicationServer напрямую и не зависят от диагностического веб-сервера.

`JobThreadWatcher` освобождает поток планировщика, если рабочая сессия ApplicationServer уже исчезла, а Quartz продолжает считать запуск активным. Компонент работает в отдельном потоке: после каждого интервала проверки получает список выполняемых заданий Quartz и список активных рабочих сессий ApplicationServer, затем сопоставляет их по идентификатору триггера. Разовое расхождение не прерывает запуск. При устойчивом расхождении наблюдатель вызывает прерывание соответствующего экземпляра Quartz job. В режиме `standby` проверка не выполняется. Интервал задает свойство `ru.bitec.jobscheduler.jobThreadWatcher.durationBetweenChecks` в конфигурации `jobscheduler`; если свойство отсутствует, используется 300 000 мс.

`SchedulerStatMonitor` позволяет оценить текущее состояние процесса без анализа его журналов: получает ли планировщик триггеры, занят ли общий пул и возникают ли проблемы связи с ApplicationServer. Для этого он хранит дату запуска процесса, размер и занятость общего пула, времена последних 100 обращений к JobStore за триггерами, количество запросов к ApplicationServer и ошибок соединения, а также сведения о последних запусках и числе активных экземпляров каждого задания. `GsfJobStore` передает монитору только время обращения за триггерами, а SOAP-задания регистрируют начало, окончание и результат обращения к ApplicationServer. Эти оперативные показатели не заменяют сведения о запусках в `Btk_JobEvent` и сводную статистику `Btk_JobStats`. Ошибки прикладного кода, полученные в успешном ответе ApplicationServer, не входят в счетчик ошибок соединения.

`SchedulerLocker` предотвращает одновременное выполнение заданий несколькими процессами `jobscheduler` в обычном режиме с REST-блокировкой. Он периодически запрашивает право выполнения у ApplicationServer: при получении блокировки запускает Quartz, при отказе переводит его в `standby`. Адрес REST-точки и интервал обращения задаются в `cQuartzProps` параметрами `restLockServerOnSchedulerReqStr` и `restSchedulerLockFrequency`. В режиме высокой доступности `SchedulerLocker` не участвует.

`GsfWebServer` позволяет администратору или внешней системе мониторинга проверить текущее состояние `jobscheduler` без доступа к интерфейсу менеджера и анализа журналов. Он предоставляет HTML-интерфейс по пути `/` и JSON-представление тех же оперативных показателей по пути `/rest/getSchedulerInfo`. REST-сервис поддерживает метод `GET`. Порт задает свойство `ru.bitec.jobscheduler.webserver.port` в конфигурации `jobscheduler`; если свойство отсутствует, используется порт `8180`. Если диагностический веб-сервер не удалось запустить, ошибка записывается в журнал, но работа Quartz продолжается.

### Технические идентификаторы

| Объект | Идентификатор |
|---|---|
| Плановый Quartz job | идентификатор `Btk_Job` |
| Плановый триггер | идентификатор `Btk_JobSchedule` |
| Конкурентный одноразовый Quartz job | `concurrent_<jobId>` |
| Одноразовый триггер | `event_<eventId>` |

Внутренний `TaskBuilder` сохраняет задания и триггеры с `replace = true`. В поддерживаемых API идентификаторы формирует менеджер заданий, поэтому прикладной модуль не должен добавлять случайный суффикс для предотвращения коллизий.

## Модель данных

**Btk_Job**

Задание хранит:

- системное имя, наименование и описание;
- признак активности и тип объекта;
- JEXL-скрипт;
- группу и пользователя выполнения;
- срок хранения событий и журналов;
- разрешенные интервалы запуска;
- общий лимит внеплановых потоков;
- признак трассировки;
- политику повторных попыток;
- признаки синхронизации и логического удаления.

`bSync` показывает, обработаны ли последние изменения конфигурации при формировании Quartz job. `bDeleted` отмечает логическое удаление. Окончательное удаление основной записи `Btk_Job` не следует использовать как публично гарантированный сценарий без дополнительной проверки реализации.

**Btk_JobSchedule**

Расписание является дочерней записью задания. Оно хранит активность, период действия, секунды, минуты, часы, дни недели, дни месяца, месяцы, тип периодичности, приращение дней и параметры повторных попыток.

Технический идентификатор планового триггера равен идентификатору `Btk_JobSchedule`.

**Btk_JobEvent**

Событие представляет один экземпляр запуска. Оно хранит:

- задание и состояние выполнения;
- даты начала и окончания;
- параметры запуска в `jParams`;
- признаки одноразового и конкурентного запуска;
- параметры повторных попыток;
- идентификатор Quartz-триггера;
- пользователя выполнения;
- группу очереди.

Плановый запуск создает событие после срабатывания расписания. Одноразовый запуск действует в обратном порядке: сначала создается `Btk_JobEvent`, затем для него формируется триггер `event_<eventId>`.

**Btk_JobEventLog и Btk_JobStats**

`Btk_JobEventLog` хранит датированные сообщения события. `Btk_JobStats` хранит последнее событие и среднее время выполнения для отображения в списке заданий.

После завершения запуска система очищает старые события с учетом `nPeriodStorageLogs` и пересчитывает статистику.

### Хранение и восстановление данных

Данные менеджера имеют разное назначение:

| Данные | Назначение |
|---|---|
| `Btk_Job`, `Btk_JobSchedule` | Исходная прикладная конфигурация |
| `Btk_JobEvent`, `Btk_JobEventLog` | История, состояние запусков и ожидающие одноразовые события |
| `Btk_JobStats` | Производная статистика для списка заданий |
| `btk_qrtz_*` | Рабочее представление Quartz |

Плановые Quartz job и триггеры могут быть повторно сформированы из конфигурации заданий. Триггеры ожидающих одноразовых событий восстанавливаются по `Btk_JobEvent`. Поэтому резервная копия только таблиц `btk_qrtz_*` не заменяет резервную копию основной базы данных.

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

## Использование в прикладном коде

### Расписания

`Btk_JobSchedule` поддерживает пять типов периодичности:

| `nPeriod` | Тип | Техническое представление |
|---|---|---|
| `0` | Однократно | `SimpleTrigger` |
| `1` | Ежедневно | `CronTrigger` |
| `2` | Еженедельно | `CronTrigger` |
| `3` | Ежемесячно | `CronTrigger` |
| `4` | Пользовательское | `CronTrigger` |

Менеджер формирует Quartz Cron-выражение из отдельных полей расписания. Значение дня месяца `32` преобразуется в `L`, то есть последний день месяца. Последний день нельзя одновременно задавать вместе с конкретными днями месяца.

Основная модель `Btk_JobSchedule` хранит элементы расписания отдельными полями. JObject-модель `Schedule` и фасад `Btk_SchedulePkg` предназначены для механизмов, которые хранят расписание в JSON: они позволяют редактировать его элементы, сформировать Cron-выражение и рассчитать следующую дату выполнения. Они не изменяют `Btk_JobSchedule` и не заменяют основное расписание менеджера заданий.

Периодический Cron-триггер использует политику пропущенного запуска `DoNothing`: пропущенный момент не выполняется задним числом. Одноразовый триггер события использует `FireNow` и выполняется при первой возможности.

**Справка по Quartz Cron**

Для низкоуровневых интеграций Quartz использует выражение из полей `секунды минуты часы день_месяца месяц день_недели`; поле года является необязательным. В стандартном интерфейсе менеджера выражение формируется автоматически из полей расписания, поэтому вводить его вручную не требуется.

Примеры выражений Quartz:

| Выражение | Результат |
|---|---|
| `0 0/5 * * * ?` | каждые пять минут |
| `10 0/5 * * * ?` | каждые пять минут на десятой секунде |
| `0 30 10-13 ? * WED,FRI` | в 10:30, 11:30, 12:30 и 13:30 по средам и пятницам |
| `0 0/30 8-9 5,20 * ?` | каждые 30 минут с 08:00 до 09:30 пятого и двадцатого числа месяца |

Символ `?` означает отсутствие конкретного значения для дня месяца или дня недели. Символ `L` обозначает последнее значение поддерживаемого периода, например последний день месяца. Выражения для прямого создания Quartz-триггеров применяйте только в специальных интеграциях; для обычного задания используйте `Btk_JobScheduleApic`. Стандартное расписание менеджера не предоставляет отдельную настройку произвольного интервала и количества повторов `SimpleTrigger`.

**Календари Quartz**

Quartz Calendar позволяет исключать даты из расписания, например праздничные дни. В прикладном пакете `Btk_QuartzPkg` предусмотрены операции добавления, получения и удаления календарей. Календарь применяется к низкоуровневому Quartz-триггеру и не заменяет расписание `Btk_JobSchedule` в стандартном менеджере заданий.

**Шаблоны расписаний**

Для прикладного кода используйте `Btk_JobScheduleApic`. Контракт предоставляет создание произвольного шаблона и типовые варианты:

- `createTemplate`;
- `templateOnceAMinute`;
- `templateMinuteInterval`;
- `templateOnceADay`.

Пример ежедневного запуска в 01:00:

<!-- Начало кода -->
```scala
val scheduleTemplate = Btk_JobScheduleApic().templateOnceADay(
  hours = 1,
  mins = 0,
  secs = 0
)
```
<!-- Конец кода -->

`templateMinuteInterval` формирует список минут внутри каждого часа. Если значение интервала не делит 60 без остатка, расстояние между последним запуском одного часа и первым запуском следующего часа будет отличаться от заданного интервала.

Не создавайте Quartz-триггер вручную для регистрации обычного задания менеджера.

### Регистрация задания

Поддерживаемая межмодульная точка регистрации — `Btk_JobApic().registerSimpleJob`.

<!-- Начало кода -->
```scala
val scheduleTemplate = Btk_JobScheduleApic().templateOnceADay(
  hours = 1,
  mins = 0,
  secs = 0
)

Btk_JobApic().registerSimpleJob(
  sSystemName = "Module_Recalculate".ns,
  sCaption = "Пересчет данных модуля".ns,
  sAction = "Module_RecalculatePkg().exec();".ns,
  scheduleTemplate = scheduleTemplate,
  sDescription = "Регулярный пересчет данных".ns,
  sGroupName = "Module".ns
)
```
<!-- Конец кода -->

Если задание с таким системным именем отсутствует, регистрация создает его и добавляет расписание. Если задание уже существует и `bRefresh = false`, метод возвращает его идентификатор без обновления настроек.

При `bRefresh = true` метод обновляет поля задания, удаляет существующие расписания и создает одно расписание из переданного шаблона. Используйте этот режим только тогда, когда регистрационный код должен быть источником конфигурации расписания.

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

Используйте стабильное системное имя. Не применяйте в новом коде `Btk_QuartzPkg.getUniquePostfix`, прямые `TaskBuilder`, `withUnscheduledThreadLimit` и другие исторические низкоуровневые примеры.

### Одноразовый запуск

`Btk_JobApic().createOneTimeTask` создает событие и возвращает его идентификатор:

<!-- Начало кода -->
```scala
val idvEvent = Btk_JobApic().createOneTimeTask(
  rpJob = ropvJob,
  mapParams = Map("documentId" -> idpDocument),
  bpConcurrent = true,
  dpStartDate = dpStartDate,
  idpExecUser = idpUser,
  bpRestart = 1.nr,
  npRestart = 3.nr,
  spGroup = spQueueGroup
)
```
<!-- Конец кода -->

`dpStartDate` задает момент, не ранее которого может начаться запуск. `bpRestart` разрешает повторные попытки, а `npRestart` задает их оставшееся количество. `spGroup` используется для распределения конкурентных запусков по группам очереди.

Для события создается триггер `event_<eventId>`. Для неконкурентного запуска идентификатор Quartz job равен идентификатору `Btk_Job`; для конкурентного используется `concurrent_<jobId>`.

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

Выключенное задание нельзя запустить одноразово. Операция «Выполнить задачу» создает новое событие и триггер, но не создает новый `Btk_Job`.

Для отмены используйте контракт:

<!-- Начало кода -->
```scala
Btk_JobApic().deleteOneTimeTask(idvEvent)
```
<!-- Конец кода -->

Параметр `bpDeleteExecuting` позволяет явно разрешить остановку уже выполняющегося события. Не удаляйте запись Quartz напрямую.

`createOneTimeTaskWithUser` устарел и не должен использоваться в новом коде.

**Параметры одноразового запуска**

`mapParams` сериализуется в JSON-поле `jParams`. Текущая реализация преобразует каждое значение в строку, а при разборе возвращает `Map[String, NString]`.

Не рассчитывайте на сохранение исходного Scala-типа. Преобразуйте значение в ожидаемый тип явно после чтения параметров текущего события.

### Фоновые процедуры

`Btk_BackgroundTaskPkg` использует системное задание `RunProcInBackground`. Фасад:

1. Формирует JEXL и параметры фоновой операции.
2. Создает одноразовый запуск.
3. Выполняет JEXL от имени заданного пользователя.
4. Сохраняет результат или стек ошибки в отдельном журнале фоновой операции.
5. После завершения отправляет уведомление через Rabbit.

Используйте этот фасад, если вызывающей стороне нужны отдельный результат фоновой операции и уведомление. Не создавайте собственный Quartz job для каждой фоновой процедуры.

## Выполнение и восстановление

### Плановое выполнение

Плановый запуск проходит следующие этапы:

1. Менеджер синхронизирует включенное задание и его активные расписания с Quartz.
2. `jobscheduler` получает из JobStore триггер, расчетное время которого наступило.
3. Планировщик формирует SOAP-запрос `executeJexl` к ApplicationServer.
4. Запрос вызывает `Btk_JobApi.execJob(jobId, scheduleId)`.
5. `execJob` автономно создает `Btk_JobEvent` в состоянии «Запланировано».
6. `Btk_JobEventApi.execEvent` проверяет, что событие можно выполнять.
7. Система определяет Quartz job и триггер, проверяет доступный интервал и переводит событие в состояние «Выполняется».
8. При включенном `bEnabledTrace` активируется трассировка серверных методов для текущего события.
9. Обработчик типа задания выполняет JEXL-скрипт или ветку очистки.
10. Результат, дата окончания, сообщение ошибки и параметры повторного запуска сохраняются автономно.
11. Система очищает устаревшие события и пересчитывает статистику.

Обычный JEXL-скрипт получает параметр `idJob` с идентификатором текущего задания:

<!-- Начало кода -->
```text
idJob -> идентификатор Btk_Job
```
<!-- Конец кода -->

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

### Журналирование и транзакции

Внутренний метод `Btk_JobEventLogApi().writeLog` сохраняет сообщение в текущей транзакции менеджера заданий. Он не входит в межмодульный контракт.

Для прикладного межмодульного кода доступна автономная запись:

<!-- Начало кода -->
```scala
Btk_JobEventLogApic().writeLogLT(
  "Обработка документа завершена".ns
)
```
<!-- Конец кода -->

`writeLogLT` создает запись в автономной транзакции и по умолчанию фиксирует ее отдельно. Используйте его, только если сообщение должно сохраниться после отката основной операции.

В JEXL-скрипте задания можно использовать оба метода:

<!-- Начало кода -->
```jexl
Btk_JobEventLogApi.writeLog("Транзакционная запись 1");
Btk_JobEventLogApi.writeLogLT("Логирующая запись 1");
session.commit();

Btk_JobEventLogApi.writeLog("Транзакционная запись 2");
Btk_JobEventLogApi.writeLogLT("Логирующая запись 2");
session.rollback();
```
<!-- Конец кода -->

После выполнения скрипта в журнале события появятся следующие сообщения:

- `Транзакционная запись 1` — запись зафиксирована вызовом `session.commit()`;
- `Логирующая запись 1` — запись сохранена в автономной транзакции;
- `Логирующая запись 2` — запись сохранена в автономной транзакции, несмотря на последующий `session.rollback()`.

Записи `Транзакционная запись 2` в журнале не будет, поскольку она относится к отмененной транзакции.

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

Вызов методов из прикладного Scala-кода также возможен, если код выполняется в контексте события задания. Для межмодульного кода используйте контракт `Btk_JobEventLogApic().writeLogLT`. Если метод вызван вне задания и для него не определено текущее событие, запись в журнал выполнения не создается.

Значимые границы транзакций:

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

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

### Восстановление и остановка

Системное задание `Btk_RestartJobEvent` каждые пять минут сопоставляет события с рабочими сессиями и триггерами. Оно:

- завершает ошибкой событие «Выполняется», если связанная сессия отсутствует;
- восстанавливает пропавший триггер запланированного одноразового события;
- создает повторную попытку, если разрешен перезапуск и значение `nRestart` положительное.

Задание восстановления само зависит от работающего планировщика. При полной остановке `jobscheduler` восстановление начнется только после возобновления его работы.

Остановка выполняющегося события ищет рабочую сессию во всем кластере и завершает ее. Отмена запланированного одноразового события удаляет триггер и переводит событие в состояние «Остановлено».

## Безопасность

Основной способ аутентификации использует JWT типа `scheduler`: `jobscheduler` подписывает его приватным ключом, а ApplicationServer проверяет публичным ключом. Устаревшие конфигурации могут использовать логин и пароль; защищенное хранилище требуется только для зашифрованного пароля.

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

Практические правила:

- назначайте отдельного пользователя с минимально необходимыми полномочиями;
- ограничивайте доступ к ключам, `cQuartzProps`, `sScript` и выбору пользователя выполнения;
- не записывайте пароли, токены и ключи в скрипт, `jParams` и журнал события;
- проверяйте входные параметры до выполнения привилегированной операции;
- не утверждайте конкретные роли без проверки полномочий целевой базы данных.

## Ограничения

- Несколько экземпляров с общим JobStore должны использовать один согласованный механизм блокировки: REST-lock или PostgreSQL advisory lock.
- В каждый момент право выполнения заданий принадлежит одному master-процессу; дополнительные процессы повышают отказоустойчивость, но не увеличивают пропускную способность одной очереди.
- Отдельные пулы конкурентных заданий увеличивают суммарное число потоков процесса; их лимиты необходимо согласовывать с ресурсами ApplicationServer и PostgreSQL.
- Очереди конкурентных групп находятся в памяти процесса и не сохраняют прежний порядок после перезапуска.
- Статистика `SchedulerStatMonitor` и сведения о расхождениях `JobThreadWatcher` находятся в памяти и сбрасываются после перезапуска процесса.
- `GsfWebServer` не выполняет встроенную аутентификацию запросов. Ограничивайте доступ к его диагностическому порту средствами инфраструктуры контура.
- Пропущенный периодический запуск не восстанавливается задним числом.
- Параметры одноразового запуска сохраняются как строки.
- Повторная попытка не отменяет данные, ранее зафиксированные прикладным скриптом.
- Конкретный unit-файл и команда установки `jobscheduler` зависят от эксплуатационной поставки; планировщик поддерживает CLI-режимы, Linux- и Windows-службы.
