Планировщик
Модуль планирования Kora позволяет запускать методы приложения по расписанию в декларативном стиле через аннотации. Во время компиляции Kora генерирует компоненты задач и связывает их с выбранным механизмом планирования.
Доступны два варианта: планировщик JDK на основе ScheduledExecutorService и планировщик на основе Quartz.
Оба поддерживают cron-выражения.
Планировщик JDK закрывает периодические и cron-задачи внутри одного приложения без дополнительных зависимостей,
а Quartz добавляет пользовательские экземпляры Trigger, подключаемый JobStore, правила выполнения задач и диалект cron от Quartz с модификаторами L, W и #.
JDK Scheduler¶
Планировщик JDK использует стандартный ScheduledExecutorService, который поставляется вместе с JDK.
Для создания задач используются специальные аннотации из пакета io.koraframework.scheduling.jdk.annotation:
@ScheduleAtFixedRate, @ScheduleWithFixedDelay, @ScheduleOnce и @ScheduleWithCron.
У всех аннотаций есть параметр config.
Если он указан, значения параметров берутся из конфигурации по этому пути и имеют приоритет над значениями из аннотации.
Конфигурация конкретной задачи также может содержать секцию telemetry, значения которой переопределяют общую телеметрию планировщика для этой задачи.
Методы, выполняемые по расписанию, должны удовлетворять следующим требованиям:
- Класс, в котором объявлен метод, должен быть компонентом в графе зависимостей, например помеченным аннотацией
@Component. - Метод планировщика
JDKне должен иметь аргументов (планировщикQuartzдополнительно допускает необязательный аргумент JobExecutionContext). - Возвращаемое значение метода игнорируется.
- В
Kotlinметод должен быть функцией-членом класса и не должен бытьsuspend-функцией.
Параметр расписания обязателен
Каждой аннотации JDK требуется расписание — либо из её собственных атрибутов, либо по пути config.
Если не задано ни то, ни другое, компиляция завершается ошибкой:
@ScheduleAtFixedRate—Either period() or config() annotation parameter must be provided@ScheduleWithFixedDelayи@ScheduleOnce—Either delay() or config() annotation parameter must be provided@ScheduleWithCron—Either value() or config() annotation parameter must be provided
Значение по умолчанию у period() и delay() равно 0, и оно считается незаданным.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Конфигурация¶
Параметры планировщика описываются классом SchedulingJdkConfig и располагаются в секции scheduling.jdk,
параметры телеметрии общие для обоих планировщиков и располагаются в секции scheduling.telemetry:
scheduling {
jdk {
shutdownWait = "30s" //(1)!
}
telemetry {
logging {
enabled = false //(2)!
}
metrics {
enabled = false //(3)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(4)!
tags = { //(5)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(6)!
attributes = { //(7)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
- Время, которое даётся пулу потоков на завершение задач перед принудительной остановкой при плавной остановке (по умолчанию:
30s) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
scheduling:
jdk:
shutdownWait: "30s" #(1)!
telemetry:
logging:
enabled: false #(2)!
metrics:
enabled: false #(3)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(4)!
tags: #(5)!
key1: value1
key2: value2
tracing:
enabled: true #(6)!
attributes: #(7)!
key1: value1
key2: value2
- Время, которое даётся пулу потоков на завершение задач перед принудительной остановкой при плавной остановке (по умолчанию:
30s) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
Пул потоков не конфигурируется: Kora создаёт ScheduledThreadPoolExecutor, размер ядра которого равен количеству задач, зарегистрированных в графе.
Его потоки называются kora-scheduler-N, не являются демонами, освобождаются после 30 секунд простоя, а отменённые задачи сразу удаляются из очереди.
Метрики модуля описаны в разделе Справочник метрик.
Конфигурация конкретной задачи также может содержать собственную секцию telemetry, которая переопределяет общую scheduling.telemetry только для этой задачи.
Незаданные значения берутся из общей конфигурации, поэтому достаточно указать только то, что должно отличаться:
Наблюдаемость задач можно настроить и в коде.
Регистрация компонента, наследующего DefaultSchedulingLoggerFactory или DefaultSchedulingMetricsFactory, меняет способ логирования или сбора метрик задач,
а регистрация компонента SchedulingTelemetryFactory полностью заменяет реализацию по умолчанию.
Фиксированная частота¶
Планирование, при котором интервал отсчитывается между началами соседних выполнений задачи.
Если выполнение длится дольше периода, следующее начнётся сразу после завершения предыдущего: выполнения одной и той же задачи никогда не накладываются друг на друга, они лишь запускаются с опозданием.
Конфигурация¶
Параметры можно передавать через конфигурацию, она имеет приоритет над значениями из аннотации.
Путь config произвольный, но по соглашению он вкладывается в секцию scheduling, чтобы параметры задачи
и её telemetry находились рядом (как в проекте с примерами, scheduling.jobs.fix-rate):
Пример файла конфигурации:
- Начальная задержка перед первой задачей (по умолчанию:
0ms) - Периодичный интервал между задачами (
обязательный, нет значения по умолчанию)
Если аннотация уже задаёт period и initialDelay, эти значения становятся значениями по умолчанию сгенерированной конфигурации,
и в конфигурации достаточно переопределить только то, что должно отличаться.
Фиксированная задержка¶
Планировщик выдерживает фиксированный интервал от момента завершения предыдущего выполнения задачи. Несколько выполнений одной и той же задачи не будут происходить одновременно.
Неважно, сколько длится текущее выполнение: следующая задача начнётся после завершения предыдущей и истечения настроенной задержки.
Конфигурация¶
Параметры можно передавать через конфигурацию, она имеет приоритет над значениями из аннотации:
Пример файла конфигурации:
- Начальная задержка перед первой задачей (по умолчанию:
0ms) - Периодичная задержка между задачами (
обязательный, нет значения по умолчанию)
Однократно¶
Однократный запуск задачи по истечении настроенного интервала времени.
Конфигурация¶
Параметры можно передавать через конфигурацию, она имеет приоритет над значениями из аннотации:
Пример файла конфигурации:
Cron¶
Планировщик JDK выполняет cron-задачи без внешнего планировщика.
Выражения разбираются и вычисляются классом CronExpression, который поставляется в артефакте scheduling-jdk.
После каждого выполнения задача вычисляет следующее время запуска от текущего момента в часовом поясе JVM по умолчанию
и планирует себя заново, поэтому долгое выполнение никогда не приводит к серии догоняющих запусков.
Формат выражения¶
Выражение может содержать пять, шесть или семь полей, разделённых пробелами.
Форма из пяти полей не содержит поля секунд и вычисляется с 0 секунд:
| Поле | Допустимые значения | Обязательное |
|---|---|---|
| Секунды | 0-59 |
в форме из шести и семи полей |
| Минуты | 0-59 |
да |
| Часы | 0-23 |
да |
| День месяца | 1-31 |
да |
| Месяц | 1-12 или JAN-DEC |
да |
| День недели | 1-7 или SUN-SAT |
да |
| Год | пусто или 1970-2099 |
нет |
В поле дня недели 1 — это воскресенье, а 7 — суббота; 0 также принимается как воскресенье.
Если поле года опущено, разрешены все годы с 1970 по 2099.
Помимо обычных чисел поддерживаются следующие специальные символы:
| Символ | Значение |
|---|---|
* |
Все значения поля (например, * в поле минут означает «каждую минуту») |
? |
Отсутствие конкретного значения, допустимо только в полях дня месяца, дня недели и года, где эквивалентно * |
, |
Перечисление значений, например 6,19 в поле часов |
- |
Включающий диапазон, например MON-FRI или 9-17 |
/ |
Шаг, например */10 в поле секунд или 5/10 |
Поля дня месяца и дня недели объединяются логическим AND, поэтому 0 0 9-17 * * MON-FRI срабатывает только по будням.
Примеры выражений:
| Выражение | Значение |
|---|---|
0 * * * * * |
В начале каждой минуты |
*/10 * * * * * |
Каждые десять секунд |
0 0 * * * ? |
В начале каждого часа |
0 0 6,19 * * ? |
В 6:00 и 19:00 каждый день |
0 0/30 8-10 * * ? |
Каждые 30 минут с 8:00 по 10:30 каждый день |
0 0 9-17 ? * MON-FRI |
Каждый час с 9:00 по 17:00 по будням |
*/15 9-17 * * MON-FRI |
Форма из пяти полей: каждые 15 минут с 9:00 по 17:00 по будням |
0 0 0 25 DEC ? |
Каждое Рождество в полночь |
0 0 0 29 FEB ? |
Каждый високосный день в полночь |
0 0 0 1 JAN ? 2027 |
1 января 2027 года в полночь |
Модификаторы Quartz не поддерживаются
Вычислитель JDK отклоняет специфичные для Quartz модификаторы L, W, # и C с ошибкой
Cron field doesn't support L, W, # or C modifiers.
Выражения, которым они нужны, должны выполняться на планировщике Quartz.
Выражение разбирается при построении графа зависимостей, а не во время компиляции,
поэтому некорректное выражение приводит к падению запуска приложения с IllegalArgumentException и указанием проблемного поля.
Если выражение больше никогда не может сработать — например, задан год в прошлом, — задача пишет предупреждение в лог и прекращает планировать себя.
Конфигурация¶
Выражение можно передавать через конфигурацию, она имеет приоритет над значением из аннотации:
Путь конфигурации принимает либо объект с ключом cron и необязательной секцией telemetry, либо обычную строку с выражением:
Если выражение указано и в аннотации, оно становится значением по умолчанию сгенерированной конфигурации, поэтому путь конфигурации может отсутствовать полностью и нужен только для переопределения расписания.
Плавная остановка¶
При плавной остановке компоненты освобождаются в обратном порядке зависимостей, поэтому каждая задача освобождается раньше исполнителя, от которого она зависит.
Освобождение задачи дожидается выполнения, идущего в этот момент, и затем отменяет расписание, никого не прерывая,
поэтому новое выполнение не начинается, а задача, которая никогда не возвращает управление, блокирует остановку.
После этого исполнитель перестаёт принимать работу и ждёт завершения пула потоков не дольше scheduling.jdk.shutdownWait;
по истечении ожидания пул останавливается принудительно, работающие потоки прерываются, а в лог пишется
SchedulingJdkExecutor failed completing graceful shutdown in ....
Поэтому долгие задачи следует писать так, чтобы они завершались сами, и дополнительно можно проверять Thread.currentThread().isInterrupted(), чтобы остановиться раньше.
Программное планирование¶
Для планирования задач в императивном стиле можно внедрить компонент SchedulingJdkExecutor.
Он оборачивает тот же пул потоков, что и аннотации, и предоставляет методы scheduleAtFixedRate, scheduleWithFixedDelay и scheduleOnce,
каждый из которых возвращает ScheduledFuture:
Задачи, запланированные таким образом, являются обычными Runnable: они делят пул с задачами из аннотаций,
но не оборачиваются в телеметрию планировщика и не отменяются по отдельности при остановке.
Quartz¶
Реализация на основе библиотеки Quartz используется для задач с пользовательскими экземплярами Trigger,
правилами выполнения Quartz и диалектом cron от Quartz.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Конфигурация¶
Сам Quartz настраивается значениями Properties в формате «ключ-значение»
в секции scheduling.quartz.properties.
Настройки Kora для плавной остановки находятся в scheduling.quartz, а телеметрия общая с планировщиком JDK и находится в секции scheduling.telemetry.
scheduling {
quartz {
waitForJobComplete = true //(1)!
properties { //(2)!
"org.quartz.threadPool.threadCount" = "10"
}
}
telemetry {
logging {
enabled = false //(3)!
}
metrics {
enabled = false //(4)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(5)!
tags = { //(6)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(7)!
attributes = { //(8)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
- Ожидать ли завершения задач перед остановкой планировщика при плавной остановке (по умолчанию:
true) - Параметры конфигурации планировщика
Quartz, накладываемые поверх значений по умолчанию ниже (опционально) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
scheduling:
quartz:
waitForJobComplete: true #(1)!
properties: #(2)!
org.quartz.threadPool.threadCount: "10"
telemetry:
logging:
enabled: false #(3)!
metrics:
enabled: false #(4)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(5)!
tags: #(6)!
key1: value1
key2: value2
tracing:
enabled: true #(7)!
attributes: #(8)!
key1: value1
key2: value2
- Ожидать ли завершения задач перед остановкой планировщика при плавной остановке (по умолчанию:
true) - Параметры конфигурации планировщика
Quartz, накладываемые поверх значений по умолчанию ниже (опционально) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
false) - Настройка SLO для метрик (по умолчанию:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настройка тегов метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настройка атрибутов трассировки (по умолчанию:
{})
Значения по умолчанию считываются из ресурса org/quartz/quartz.properties, поставляемого с библиотекой Quartz, и затем корректируются Kora.
Любой ключ, присутствующий в scheduling.quartz.properties, имеет приоритет над обоими источниками.
Свойства, задаваемые Kora
Конфигурация конкретной cron-задачи также может содержать секцию telemetry, значения которой переопределяют общую телеметрию планировщика для этой задачи,
ровно так же, как описано для планировщика JDK.
Cron¶
Для запуска задач по расписанию используются cron-выражения.
Выражение Quartz состоит из шести обязательных полей и необязательного седьмого поля года, разделённых пробелами:
| Поле | Допустимые значения | Обязательное |
|---|---|---|
| Секунды | 0-59 |
да |
| Минуты | 0-59 |
да |
| Часы | 0-23 |
да |
| День месяца | 1-31 |
да |
| Месяц | 1-12 или JAN-DEC |
да |
| День недели | 1-7 или SUN-SAT |
да |
| Год | пусто, 1970-2099 |
нет |
Помимо обычных чисел, диапазонов (8-10), перечислений (6,19) и шагов (0/30) поддерживаются следующие специальные символы:
| Символ | Значение |
|---|---|
* |
Все значения поля (например, * в поле минут означает «каждую минуту») |
? |
Отсутствие конкретного значения, используется в поле дня месяца или дня недели, когда задано другое из них |
L |
Последний (последний день месяца или последний указанный день недели в месяце) |
W |
Ближайший рабочий день к указанному дню месяца |
# |
N-й указанный день недели в месяце, например 5#2 — вторая пятница |
Примеры выражений:
| Выражение | Значение |
|---|---|
0 0 * * * ? |
В начале каждого часа каждого дня |
*/10 * * * * ? |
Каждые десять секунд |
0 0 8-10 * * ? |
В 8, 9 и 10 часов каждый день |
0 0/30 8-10 * * ? |
В 8:00, 8:30, 9:00, 9:30, 10:00 и 10:30 |
0 0 0 L * ? |
В последний день месяца в полночь |
0 0 0 1W * ? |
В первый рабочий день месяца в полночь |
0 0 0 ? * 5#2 |
Во вторую пятницу месяца в полночь |
Атрибут identity задаёт идентификатор Quartz Trigger,
которым именуется задача — это полезно для идентификации и замены задач, особенно с кластерными или персистентными реализациями JobStore.
Если он не задан, идентификатором становится полное имя класса и имя метода задачи, например com.example.SomeService#schedule:
Источник cron обязателен
@ScheduleWithCron должна получить выражение либо из value(), либо из config().
Если не задано ни то, ни другое, компиляция завершается ошибкой Quartz @ScheduleWithCron on '...' has no cron source.
Конфигурация¶
Параметры можно передавать через конфигурацию, она имеет приоритет над значениями из аннотации.
Как и для планировщика JDK, путь config произвольный и по соглашению вкладывается в секцию scheduling
(как в проекте с примерами, scheduling.jobs.quartz):
Путь конфигурации принимает либо объект с ключом cron и необязательной секцией telemetry, либо обычную строку с выражением:
Trigger¶
Для собственного расписания можно создать Trigger из библиотеки Quartz, зарегистрировать его в графе зависимостей с тегом
и затем передать класс этого тега в аннотацию @ScheduleWithTrigger.
@KoraApp
public interface Application extends QuartzModule {
@Tag(SomeService.class) //(1)!
default Trigger myTrigger() {
return TriggerBuilder.newTrigger()
.withIdentity("myTrigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(50)
.repeatForever())
.build();
}
}
@Component
public class SomeService {
@ScheduleWithTrigger(SomeService.class) //(2)!
void schedule() {
// do something
}
}
- Тег, с которым
Triggerрегистрируется в графе зависимостей. - Тот же тег, по которому задача получает
Trigger.
@KoraApp
interface Application : QuartzModule {
@Tag(SomeService::class) //(1)!
fun myTrigger(): Trigger {
return TriggerBuilder.newTrigger()
.withIdentity("myTrigger")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(50)
.repeatForever()
)
.build()
}
}
@Component
class SomeService {
@ScheduleWithTrigger(SomeService::class) //(2)!
fun schedule() {
// do something
}
}
- Тег, с которым
Triggerрегистрируется в графе зависимостей. - Тот же тег, по которому задача получает
Trigger.
У @ScheduleWithTrigger нет атрибута config: всё расписание выражается самим компонентом Trigger.
Неконкурентное выполнение¶
Аннотация @DisallowConcurrentExecution запрещает одновременное выполнение одного и того же метода планировщиком Quartz.
Это аналог org.quartz.DisallowConcurrentExecution в Kora, он ставится на метод, помеченный @Schedule*;
исходная аннотация org.quartz.DisallowConcurrentExecution на классе даёт тот же эффект для всех его задач.
Контекст задачи¶
Метод задачи Quartz может опционально объявить единственный аргумент типа org.quartz.JobExecutionContext.
Если он присутствует, Kora передаёт в метод текущий контекст выполнения; если отсутствует, метод вызывается без аргументов.
Контекст даёт доступ к org.quartz.JobDataMap задачи — это способ читать и записывать состояние, связанное с задачей:
@Component
public class SomeService {
@ScheduleWithCron(config = "scheduling.jobs.quartz")
void schedule(JobExecutionContext context) {
JobDataMap data = context.getJobDetail().getJobDataMap();
int counter = data.containsKey("counter") ? data.getInt("counter") : 0;
data.put("counter", counter + 1);
}
}
Сохранение данных задачи¶
Аннотация @PersistJobDataAfterExecution указывает Quartz сохранить обновлённый org.quartz.JobDataMap после выполнения задачи,
чтобы изменения, сделанные через JobExecutionContext, были видны в следующем выполнении.
Её рекомендуется использовать вместе с @DisallowConcurrentExecution,
чтобы избежать конфликтов хранения данных при одновременном выполнении задачи.
@Component
public class SomeService {
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
@ScheduleWithCron(config = "scheduling.jobs.quartz")
void schedule(JobExecutionContext context) {
JobDataMap data = context.getJobDetail().getJobDataMap();
int counter = data.containsKey("counter") ? data.getInt("counter") : 0;
data.put("counter", counter + 1); //(1)!
}
}
- Обновлённое значение сохраняется после выполнения и доступно в следующем запуске
@Component
class SomeService {
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
@ScheduleWithCron(config = "scheduling.jobs.quartz")
fun schedule(context: JobExecutionContext) {
val data = context.jobDetail.jobDataMap
val counter = if (data.containsKey("counter")) data.getInt("counter") else 0
data.put("counter", counter + 1) //(1)!
}
}
- Обновлённое значение сохраняется после выполнения и доступно в следующем запуске
Плавная остановка¶
При плавной остановке опция scheduling.quartz.waitForJobComplete определяет, как останавливается планировщик Quartz.
При значении true (по умолчанию) вызывается scheduler.shutdown(true) и остановка блокируется до завершения выполняющихся задач; при false планировщик останавливается без ожидания.
Как и в случае планировщика JDK, долгие задачи всё равно должны кооперативно проверять
Thread.currentThread().isInterrupted() и завершать работу самостоятельно.
Scheduler¶
Нижележащий org.quartz.Scheduler зарегистрирован как компонент и может быть внедрён для продвинутых сценариев,
таких как программная регистрация задач или анализ состояния планировщика:
Каждая объявленная задача регистрируется как durable JobDetail, идентификатором которого является каноническое имя сгенерированного класса задачи.
Регистрация повторяется при обновлении графа зависимостей: триггеры с изменившимся определением перепланируются,
а триггеры, которых больше нет, удаляются.