Планировщик
Модуль планирования Kora позволяет запускать методы приложения по расписанию в декларативном стиле через аннотации. Во время компиляции Kora генерирует компоненты задач и связывает их с выбранным механизмом планирования.
Доступны два варианта: собственный планировщик на основе ScheduledExecutorService из JDK и планировщик на основе Quartz.
Собственный вариант подходит для простых периодических задач внутри одного приложения, а Quartz полезен для cron-выражений, пользовательских экземпляров Trigger и дополнительных правил выполнения задач.
Собственный планировщик¶
Собственный планировщик использует стандартный ScheduledExecutorService, который поставляется вместе с JDK.
Для создания задач через аспекты используются специальные аннотации, соответствующие методам ScheduledExecutorService.
Параметры аннотаций совпадают с параметрами методов scheduleAtFixedRate, scheduleWithFixedDelay и schedule.
У всех аннотаций есть параметр config.
Если он указан, значения параметров берутся из конфигурации по этому пути и имеют приоритет над значениями из аннотации.
Конфигурация конкретной задачи также может содержать секцию telemetry, значения которой переопределяют общую телеметрию планировщика для этой задачи.
Методы, выполняемые по расписанию, должны удовлетворять следующим требованиям:
- Класс, в котором объявлен метод, должен быть компонентом в графе зависимостей, например помеченным аннотацией
@Component. - Метод собственного планировщика не должен иметь аргументов (планировщик
Quartzдополнительно допускает необязательный аргумент JobExecutionContext). - Возвращаемое значение метода игнорируется.
- В
Kotlinметод не должен бытьsuspend-функцией.
Интервал обязателен
@ScheduleAtFixedRate требует period, а @ScheduleWithFixedDelay требует delay.
Если не задан ни атрибут аннотации (его значение по умолчанию равно 0), ни путь config, предоставляющий значение,
компиляция завершается ошибкой Either period() or config() annotation parameter must be provided.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Конфигурация¶
Полный пример конфигурации, описываемой классом ScheduledExecutorServiceConfig, со значениями по умолчанию:
scheduling {
threads = 2 //(1)!
shutdownWait = "30s" //(2)!
telemetry {
logging {
enabled = false //(3)!
}
metrics {
enabled = true //(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"
}
}
}
}
- Максимальное количество потоков в ScheduledExecutorService (по умолчанию:
2) - Время ожидания завершения задач перед остановкой планировщика при плавной остановке (по умолчанию:
30s) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настраивает SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настраивает теги метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настраивает атрибуты трассировки (по умолчанию:
{})
scheduling:
threads: 2 #(1)!
shutdownWait: "30s" #(2)!
telemetry:
logging:
enabled: false #(3)!
metrics:
enabled: true #(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
- Максимальное количество потоков в ScheduledExecutorService (по умолчанию:
2) - Время ожидания завершения задач перед остановкой планировщика при плавной остановке (по умолчанию:
30s) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настраивает SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настраивает теги метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настраивает атрибуты трассировки (по умолчанию:
{})
Метрики модуля описаны в разделе Справочник метрик.
Конфигурация конкретной задачи также может содержать собственную секцию telemetry, которая переопределяет общую для планировщика scheduling.telemetry только для этой задачи.
Незаданные значения берутся из общей конфигурации, поэтому достаточно указать только то, что должно отличаться:
Наблюдаемость задач по расписанию также можно настроить в коде, зарегистрировав компонент, реализующий
SchedulingLoggerFactory, SchedulingMetricsFactory, SchedulingTracerFactory или целиком SchedulingTelemetryFactory.
Фиксированная частота¶
Планирование с запуском задач через фиксированный интервал времени независимо от того, завершилось ли предыдущее выполнение. Это может приводить к одновременному выполнению нескольких задач.
Например, если период равен 10 секундам, а каждое выполнение задачи занимает 5 секунд, то следующая задача запускается через 5 секунд после завершения предыдущей.
Конфигурация¶
Параметры можно передавать через конфигурацию; конфигурация имеет приоритет над значениями из аннотации.
Путь config произвольный, но по соглашению вкладывается в секцию scheduling, чтобы параметры задачи
и её telemetry находились вместе (как в проекте-примере, scheduling.jobs.fix-rate):
Пример файла конфигурации:
- Начальная задержка перед первой задачей (по умолчанию:
0ms) - Периодический интервал между задачами (
обязательный, без значения по умолчанию)
Фиксированная задержка¶
Планировщик выдерживает фиксированный интервал времени от момента окончания предыдущего выполнения задачи. Несколько выполнений одной и той же задачи не будут происходить одновременно.
Не имеет значения, сколько длится текущее выполнение: следующая задача запускается после того, как предыдущая задача завершилась и прошла настроенная задержка.
Конфигурация¶
Параметры можно передавать через конфигурацию; она имеет приоритет над значениями из аннотации:
Пример файла конфигурации:
- Начальная задержка перед первой задачей (по умолчанию:
0ms) - Периодическая задержка между задачами (
обязательный, без значения по умолчанию)
Однократно¶
Запускает задачу один раз через настроенный интервал времени.
Конфигурация¶
Параметры можно передавать через конфигурацию; она имеет приоритет над значениями из аннотации:
Пример файла конфигурации:
Плавная остановка¶
Во время плавной остановки собственный планировщик ожидает завершения задач в течение scheduling.shutdownWait.
Если задачу нужно остановить раньше, проверяйте Thread.currentThread().isInterrupted() и останавливайте работу вручную.
Программное планирование¶
Для планирования задач в императивном стиле можно внедрить компонент JdkSchedulingExecutor.
Он оборачивает тот же ScheduledExecutorService, что и аннотации, и предоставляет методы scheduleAtFixedRate, scheduleWithFixedDelay и schedule:
Quartz¶
Реализация на основе библиотеки Quartz используется для задач с расписанием по cron, пользовательских экземпляров Trigger и правил выполнения Quartz.
Подключение¶
Зависимость build.gradle:
Модуль:
Зависимость build.gradle.kts:
Модуль:
Конфигурация¶
Конфигурация Quartz задаётся значениями Properties в формате ключ-значение.
Настройки Kora для плавной остановки и телеметрии задаются в секции scheduling.
Конфигурация конкретной cron-задачи также может содержать секцию telemetry, значения которой переопределяют общую телеметрию планировщика для этой задачи.
quartz { //(1)!
"org.quartz.threadPool.threadCount" = "10"
}
scheduling {
waitForJobComplete = true //(2)!
telemetry {
logging {
enabled = false //(3)!
}
metrics {
enabled = true //(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"
}
}
}
}
- Параметры конфигурации планировщика
Quartz(по умолчанию используются свойства изquartz.propertiesниже) - Ожидать ли завершения задач перед остановкой планировщика при плавной остановке (по умолчанию:
true) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настраивает SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настраивает теги метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настраивает атрибуты трассировки (по умолчанию:
{})
quartz: #(1)!
org.quartz.threadPool.threadCount: "10"
scheduling:
waitForJobComplete: true #(2)!
telemetry:
logging:
enabled: false #(3)!
metrics:
enabled: true #(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
- Параметры конфигурации планировщика
Quartz(по умолчанию используются свойства изquartz.propertiesниже) - Ожидать ли завершения задач перед остановкой планировщика при плавной остановке (по умолчанию:
true) - Включает логирование модуля (по умолчанию:
false) - Включает метрики модуля (по умолчанию:
true) - Настраивает SLO для метрик (по умолчанию:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Настраивает теги метрик (по умолчанию:
{}) - Включает трассировку модуля (по умолчанию:
true) - Настраивает атрибуты трассировки (по умолчанию:
{})
Настройки по умолчанию используются из:
quartz.properties
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 60000
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
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:
Конфигурация¶
Параметры можно передавать через конфигурацию; конфигурация имеет приоритет над значениями из аннотации.
Как и в случае собственного планировщика, путь config произвольный и по соглашению вкладывается в секцию scheduling
(как в проекте-примере, scheduling.jobs.quartz):
Пример конфигурации:
cron-выражение, которое запускает задачу каждую секунду (обязательный, без значения по умолчанию)
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(@Tag(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(@Tag(SomeService::class)) //(2)!
fun schedule() {
// do something
}
}
- Тег, используемый для регистрации
Triggerв графе зависимостей. - Тот же тег, используемый задачей для получения
Trigger.
Неконкурентное выполнение¶
Аннотация @DisallowConcurrentExecution предотвращает одновременное выполнение одного и того же метода планировщиком Quartz.
Это аналог org.quartz.DisallowConcurrentExecution в Kora, который можно разместить на любом методе, помеченном @Schedule*.
Контекст задачи¶
Метод, выполняемый по расписанию 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.waitForJobComplete управляет тем, как останавливается планировщик Quartz.
При true (по умолчанию) он вызывает scheduler.shutdown(true) и блокируется до завершения выполняющихся задач; при false он останавливается без ожидания.
Как и в случае собственного планировщика, длительно выполняющиеся задачи всё же должны кооперативно проверять
Thread.currentThread().isInterrupted() и останавливать работу вручную.
Scheduler¶
Лежащий в основе org.quartz.Scheduler регистрируется как компонент и может быть внедрён для продвинутых сценариев,
таких как программная регистрация задач или инспекция состояния планировщика: