# Планировщик задач DocScheduler (CRON_TABLE)

Планировщик `DocScheduler` предназначен для периодического запуска задач (CSP‑сервисов или DB‑функций) по расписанию, задаваемому с помощью SQL‑выражений. Управление задачами осуществляется через документы типа **CRON_TABLE**.

Планировщик автоматически отслеживает выполнение запущенных процессов, фиксирует ошибки, рассчитывает следующее время запуска и при необходимости переводит задачу в неактивное состояние (по превышению лимита ошибок или невалидному расписанию).

---

## Документ CRON_TABLE: поля и их назначение

Для создания задачи необходимо добавить документ с `dockind = "CRON_TABLE"` и статусом `"CRON_TABLE_ACTIVE"`.  
Поддерживаются следующие свойства:

| Поле | Тип | Описание |
|------|-----|-----------|
| `NUM` | число/строка | Идентификатор (номер) задачи. |
| `DESCR` | строка | Описание задачи (назначение). |
| `SYS_PROCESS_ID` | число | ID запущенного системного процесса (заполняется автоматически). |
| `DB_FUNCTION` | строка | Имя DB‑функции для вызова (если не используется CSP). |
| `CSP_SERVICE` | строка | Имя CSP‑сервиса для вызова. |
| `CSP_SERVICE_PARAMS` | строка (JSON) | Параметры вызова CSP‑сервиса в формате JSON. |
| `INTERVAL` | строка | **SQL‑выражение**, вычисляющее следующую дату/время запуска (см. ниже). |
| `INTERVAL_ERROR` | строка | SQL‑выражение для следующего запуска **после ошибки** (если не задано, используется `INTERVAL`). |
| `MAX_COUNT_ERROR` | число | Максимальное количество последовательных ошибок, после которого задача переводится в статус `CRON_TABLE_ERROR`. |
| `COUNT_ERROR` | число | Текущий счётчик последовательных ошибок (заполняется автоматически). |
| `FIRST_RUN` | datetime | Первый запланированный запуск (опционально, используется для проверки). |
| `LAST_RUN` | datetime | Время последнего запуска (заполняется автоматически). |
| `NEXT_RUN` | datetime | Следующее время запуска (рассчитывается автоматически). |
| `DAILY_START_TIME` | datetime | Время фактического начала выполнения задачи (заполняется автоматически). |
| `DAILY_END_TIME` | datetime | Время окончания выполнения задачи (заполняется автоматически). |
| `ERROR_TEXT` | строка | Текст последней ошибки (заполняется автоматически). |
| `PERIODICAL_RUN` | *не используется* | (зарезервировано) |
| `DEL_ON_FINISH` | *не используется* | (зарезервировано) |

> **Важно:** Для запуска задачи необходимо указать **либо** `CSP_SERVICE` (с возможными `CSP_SERVICE_PARAMS`), **либо** `DB_FUNCTION`.

---

## Принцип работы планировщика

### 1. Загрузка задач

При старте и далее каждые **10 минут или при изменениях в CRON_TABLE** (таймер `reloadTimer`) планировщик перечитывает все активные документы `CRON_TABLE_ACTIVE`.  
Также перезагрузка происходит при любом изменении статуса системного процесса (через триггер `trgProcessChanged`) — например, после завершения задачи.

### 2. Цикл проверки расписания

Каждые **30 секунд** (таймер `checkTimer`) планировщик:
- Собирает задачи, у которых `NEXT_RUN <= текущего времени` **и** `SYS_PROCESS_ID` отсутствует (т.е. процесс не запущен).
- Запускает полную перезагрузку задач и их обработку, при наличии собранных задач

### 3. Обработка задачи (`_processTask`)

Для каждой активной задачи выполняется логика:

```
Если NEXT_RUN в будущем → пропустить
Иначе если SYS_PROCESS_ID отсутствует → запустить задачу (_runTask)
Иначе если процесс с таким ID не найден в sysprocs → запустить задачу заново
Иначе если процесс завершён с ошибкой → вызвать _finishTask с текстом ошибки
Иначе если процесс завершён успешно → вызвать _finishTask без ошибки
Иначе процесс ещё выполняется → ничего не делать
```

### 4. Запуск задачи (`_runTask`)

- Создаётся новый системный процесс:
  - тип `csp` – для вызова CSP‑сервиса (параметры берутся из `CSP_SERVICE_PARAMS`);
  - тип `db` – для вызова DB‑функции.
- В документе CRON_TABLE проставляется `SYS_PROCESS_ID` и текущее время в `DAILY_START_TIME`.
- После этого планировщик перезагружает списки задач и процессов.

Если на этапе запуска возникает исключение, вызывается `_finishTask` с текстом ошибки.

### 5. Завершение задачи / расчёт следующего запуска (`_finishTask`)

**Шаги:**

1. **Определение интервала**  
   - Если есть ошибка → используется `INTERVAL_ERROR` (или `INTERVAL`, если `INTERVAL_ERROR` не задан).  
   - Если ошибки нет → используется `INTERVAL`.

2. **Обновление счётчика ошибок**  
   - При ошибке: `COUNT_ERROR` увеличивается на 1.  
   - При успехе: `COUNT_ERROR` сбрасывается в 0.

3. **Проверка лимита ошибок**  
   - Если `COUNT_ERROR > MAX_COUNT_ERROR` → задача переводится в статус `CRON_TABLE_ERROR` и больше не запускается.

4. **Расчёт следующего времени запуска**  
   - Выполняется SQL‑запрос: `SELECT (<INTERVAL_выражение>) as RESULT`.  
   - Результат преобразуется в `dayjs`‑объект.  
   - Если выражение пустое, результат невалиден или запрос не удался → задача переводится в статус `CRON_TABLE_PASSIVE` (отключена).  
   - Иначе `NEXT_RUN` устанавливается на вычисленную дату/время.

5. **Обновление документа**  
   - Сбрасывается `SYS_PROCESS_ID`, проставляется `DAILY_END_TIME`, `ERROR_TEXT`, новый `NEXT_RUN` и `COUNT_ERROR`.  
   - При необходимости изменяется статус документа (на `CRON_TABLE_ERROR` или `CRON_TABLE_PASSIVE`).  
   - Выполняется перезагрузка списков задач и процессов.

---

## Настройка расписания (INTERVAL и INTERVAL_ERROR)

Планировщик **не использует классические cron‑выражения**. Вместо этого для вычисления следующей даты запуска применяются **произвольные SQL‑выражения**, возвращающие timestamp.

### Формат

Выражение должно быть валидным SQL‑выражением для СУБД, на которой работает MorphCluster. Рекомендуется использовать `NOW()` как точку отсчёта.

### Примеры

| Цель | SQL‑выражение для `INTERVAL` |
|------|-------------------------------|
| Каждые 5 минут | `NOW() + INTERVAL '5 minutes'` |
| Каждый час | `NOW() + INTERVAL '1 hour'` |
| Ежедневно в 03:00 | `DATE_TRUNC('day', NOW()) + INTERVAL '1 day' + INTERVAL '3 hours'` |
| Каждый понедельник в 09:00 | `NEXT_DAY(NOW(), 'MONDAY') + INTERVAL '9 hours'` (зависит от диалекта SQL) |
| Через 1 день после успешного выполнения | `NOW() + INTERVAL '1 day'` |

### Особенность при пустом `INTERVAL`

Если `INTERVAL` (и `INTERVAL_ERROR`) не заданы, то:
- После успешного выполнения задачи `NEXT_RUN` останется `NULL`.
- При следующем цикле проверки планировщик будет считать, что `NEXT_RUN` уже наступил (условие `task.NEXT_RUN && task.NEXT_RUN.isAfter(...)` не сработает, т.к. `NEXT_RUN` — `null`).
- В результате задача **будет запускаться повторно сразу после предыдущего завершения** (зацикливание).

**Рекомендация:** всегда задавайте явное `INTERVAL` или переводите задачу в неактивный статус вручную после разового выполнения.

---

## Обработка ошибок и отказоустойчивость

- **Счётчик ошибок** (`COUNT_ERROR`) увеличивается при любом сбое:
  - Ошибка запуска задачи (исключение в `_runTask`).
  - Завершение системного процесса со статусом `failed`.
- **Раздельный интервал после ошибки** позволяет задать более частое повторение (например, `NOW() + INTERVAL '5 minutes'`) или более длительную паузу.
- **Лимит ошибок** (`MAX_COUNT_ERROR`): по достижении лимита задача автоматически деактивируется (статус `CRON_TABLE_ERROR`). Это предотвращает бесконечные попытки заведомо ошибочной задачи.
- **Невалидное расписание** (ошибка вычисления `NEXT_RUN` или пустой `INTERVAL`) переводит задачу в статус `CRON_TABLE_PASSIVE` — она исключается из обработки до ручного вмешательства.

---

## Жизненный цикл задачи (на примере)

1. **Создание**  
   Добавляется документ `CRON_TABLE` со статусом `ACTIVE`, заполняются поля `CSP_SERVICE`/`DB_FUNCTION`, `INTERVAL`, `MAX_COUNT_ERROR` (опционально).

2. **Первый запуск**  
   - Если `NEXT_RUN` не задан или `NEXT_RUN <= текущего времени` → планировщик запускает задачу.  
   - `SYS_PROCESS_ID` получает ID нового системного процесса.  
   - `DAILY_START_TIME` фиксирует момент старта.

3. **Выполнение**  
   Планировщик не вмешивается в ход работы процесса. Процесс выполняется асинхронно.

4. **Завершение процесса**  
   - Системный процесс переходит в статус `completed` или `failed`.  
   - Триггер `trgProcessChanged` немедленно вызывает перезагрузку планировщика.  
   - Планировщик обрабатывает завершённую задачу через `_finishTask`:
     - Рассчитывается `NEXT_RUN` (с учётом ошибки, если была).  
     - Обновляются `COUNT_ERROR`, `DAILY_END_TIME`, `ERROR_TEXT`.  
     - Сбрасывается `SYS_PROCESS_ID`.  
     - При необходимости меняется статус документа.

5. **Повторный запуск**  
   Когда текущее время достигнет нового `NEXT_RUN`, планировщик снова запустит задачу.

6. **Отключение задачи**  
   - Автоматически: при превышении `MAX_COUNT_ERROR` или невалидном `INTERVAL`.  
   - Вручную: изменить статус документа на любой, кроме `CRON_TABLE_ACTIVE`.

---

## Особенности и ограничения

### 1. Механизм блокировки повторной перезагрузки
- Используется флаг `reloading` и отложенный вызов `needReload` для предотвращения одновременной перезагрузки из разных таймеров/триггеров.

### 2. Сессия выполнения
- Задачи запускаются от имени пользователя `userId = 5056` с правами администратора (`isAdmin: true`).  
- Это зашито в коде и не настраивается через CRON_TABLE.

### 3. Поведение при `NEXT_RUN = NULL`
Как уже отмечено, это приводит к немедленному повторному запуску после завершения. Если вам нужно **однократное выполнение**, после успеха следует вручную перевести задачу в статус `CRON_TABLE_PASSIVE` или `CRON_TABLE_ERROR` (например, через отдельный процесс).

### 4. Типы вызываемых функций
- **CSP‑сервис** – должен быть зарегистрирован в системе. Параметры передаются через `CSP_SERVICE_PARAMS` в виде JSON-строки.  
- **DB‑функция** – хранимая функция в базе данных. Вызов происходит без параметров (но можно передать через глобальные переменные сессии или отдельный механизм).

---

## Рекомендации по настройке

1. **Всегда задавайте `INTERVAL`** даже для периодических задач. Для разовых задач используйте ручное отключение или запланируйте удаление документа после выполнения.
2. **Указывайте `MAX_COUNT_ERROR`** – разумное значение (например, 3–5) для защиты от «зависших» ошибочных задач.
3. **Используйте `INTERVAL_ERROR`**, если после сбоя нужно повторить попытку быстрее, чем обычно.
4. **Проверяйте SQL‑выражения** в консоли базы данных перед внесением в `INTERVAL`.
5. **Избегайте слишком частых запусков** менее 30 секунд

---

## Пример документа CRON_TABLE

```json
{
  "dockind": "CRON_TABLE",
  "status": "CRON_TABLE_ACTIVE",
  "props": {
    "NUM": 101,
    "DESCR": "Ежечасная архивация логов",
    "DB_FUNCTION": "archive_logs",
    "INTERVAL": "NOW() + INTERVAL '1 hour'",
    "INTERVAL_ERROR": "NOW() + INTERVAL '5 minutes'",
    "MAX_COUNT_ERROR": 3,
    "FIRST_RUN": "2025-01-01T00:00:00"
  }
}
```

Этот документ заставит планировщик вызывать DB‑функцию `archive_logs` каждый час. При возникновении ошибки следующая попытка будет через 5 минут. После трёх последовательных ошибок задача перейдёт в статус `CRON_TABLE_ERROR`.

---

## Заключение

Планировщик DocScheduler предоставляет гибкий механизм запуска задач по расписанию на основе SQL‑выражений. Настройка через документы CRON_TABLE позволяет динамически добавлять, изменять и отключать задачи без перезапуска сервиса. Важно правильно заполнять поля `INTERVAL` и контролировать обработку ошибок через `MAX_COUNT_ERROR` и `INTERVAL_ERROR`.