Обработка сбоев заданий в Quartz с повторными попытками

Обработка сбоев заданий в Quartz с повторными попытками

10 февраля 2023 г.

Quartz — это платформа планирования заданий с открытым исходным кодом, которую можно интегрировать в самые разные приложения Java. Он используется для планирования заданий, которые можно выполнять позже или периодически.

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

Эта статья основана на сообщении в блоге Харви Делани о повторных попытках исключения заданий в Quartz.NET. Здесь мы рассматриваем использование Quartz в приложении Spring и реализуем политику экспоненциальной случайной отсрочки, которая уменьшает количество попыток повторных попыток, предотвращает перегрузку системы, позволяет системе медленно восстанавливаться с течением времени и равномерно распределяет попытки повторных попыток.

Сохранение параметров повтора

Мы будем использовать JobDataMap-s, связанные с объектами JobDetail и Trigger нашего задания. JobDetail JobDataMap будет хранить случайные экспоненциальные параметры политики отсрочки MAX_RETRIES, RETRY_INITIAL_INTERVAL_SECS и RETRY_INTERVAL_MULTIPLIER. JobDataMap, связанный с триггером, будет хранить RETRY_NUMBER.

Прослушиватель обработки сбоя задания

Теперь добавим компонент Spring JobFailureHandlingListener, реализующий интерфейс org.quartz.JobListener. JobListener#jobToBeExecuted вызывается, когда задание должно быть выполнено, а JobListener#jobWasExecuted вызывается после завершения задания. В jobToBeExecuted мы увеличим счетчик RETRY_NUMBER. jobWasExecuted будет содержать основной код обработки исключений и создания нового триггера для следующего выполнения задания.

Политика экспоненциальной случайной отсрочки

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

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

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

Покажите мне код

JobFailureHandlingListener.java

package quartzdemo.listeners;

import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JobFailureHandlingListener implements JobListener {

    private final String RETRY_NUMBER_KEY = "RETRY_NUMBER";
    private final String MAX_RETRIES_KEY = "MAX_RETRIES";
    private final int DEFAULT_MAX_RETRIES = 5;
    private final String RETRY_INITIAL_INTERVAL_SECS_KEY = "RETRY_INITIAL_INTERVAL_SECS";
    private final int DEFAULT_RETRY_INITIAL_INTERVAL_SECS = 60;
    private final String RETRY_INTERVAL_MULTIPLIER_KEY = "RETRY_INTERVAL_MULTIPLIER";
    private final double DEFAULT_RETRY_INTERVAL_MULTIPLIER = 1.5;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public String getName() {
        return "FailJobListener";
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        context.getTrigger().getJobDataMap().merge(RETRY_NUMBER_KEY, 1,
                (oldValue, initValue) -> ((int) oldValue) + 1);
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) { }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        if (context.getTrigger().getNextFireTime() != null || jobException == null) {
            return;
        }
        int maxRetries = (int) context.getJobDetail().getJobDataMap()
                .computeIfAbsent(MAX_RETRIES_KEY, key -> DEFAULT_MAX_RETRIES);
        int timesRetried = (int) context.getTrigger().getJobDataMap().get(RETRY_NUMBER_KEY);
        if (timesRetried > maxRetries) {
            logger.error("Job with ID and class: " + context.getJobDetail().getKey() +", " + context.getJobDetail().getJobClass() +
                    " has run " + maxRetries + " times and has failed each time.", jobException);
            return;
        }

        TriggerKey triggerKey = context.getTrigger().getKey();
        int initialIntervalSecs = (int) context.getJobDetail().getJobDataMap()
                .computeIfAbsent(RETRY_INITIAL_INTERVAL_SECS_KEY, key -> DEFAULT_RETRY_INITIAL_INTERVAL_SECS);
        double multiplier = (double) context.getJobDetail().getJobDataMap()
                .computeIfAbsent(RETRY_INTERVAL_MULTIPLIER_KEY, key -> DEFAULT_RETRY_INTERVAL_MULTIPLIER);
        Date newStartTime = ExponentialRandomBackoffFixtures.getNextStartDate(timesRetried, initialIntervalSecs, multiplier);
        Trigger newTrigger = TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)
                .startAt(newStartTime)
                .usingJobData(context.getTrigger().getJobDataMap())
                .build();
        newTrigger.getJobDataMap().put(RETRY_NUMBER_KEY, timesRetried);

        try {
            context.getScheduler().rescheduleJob(triggerKey, newTrigger);
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

}

ExponentialRandomBackoffFixtures.java

package quartzdemo.listeners;

import org.apache.commons.lang3.RandomUtils;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

public class ExponentialRandomBackoffFixtures {

    public static Date getNextStartDate(int timesRetried, int initialIntervalSecs, double multiplier) {
        double minValue = initialIntervalSecs * Math.pow(multiplier, timesRetried - 1);
        double maxValue = minValue * multiplier;
        Duration duration = Duration.ofMillis((long) (RandomUtils.nextDouble(minValue, maxValue) * 1000));
        LocalDateTime nextDateTime = LocalDateTime.now().plus(duration);
        return Date.from(nextDateTime.atZone(ZoneId.systemDefault()).toInstant());
    }

}

Теперь мы можем добавить прослушиватель в планировщик

package quartzdemo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import quartzdemo.listeners.JobFailureHandlingListener;

@Configuration
@EnableScheduling
public class SchedulingConfiguration {

    private final JobFailureHandlingListener jobFailureHandlingListener;

    public SchedulingConfiguration2(JobFailureHandlingListener jobFailureHandlingListener) {
        this.jobFailureHandlingListener = jobFailureHandlingListener;
    }

    @Bean
    public SchedulerFactoryBean scheduler() {
        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
        // ...
        schedulerFactory.setGlobalJobListeners(jobFailureHandlingListener);
        return schedulerFactory;
    }

}

и запустить задание с пользовательским параметром RETRY_INITIAL_INTERVAL_SECS

package quartzdemo.services;

import org.quartz.*;
import org.springframework.stereotype.Service;
import quartzdemo.jobs.SimpleJob;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

import static quartzdemo.listeners.JobFailureHandlingListener.RETRY_INITIAL_INTERVAL_SECS_KEY;

@Service
public class SchedulingService {

    private final Scheduler scheduler;

    public SchedulingService(Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    // ...

    private void scheduleJob() throws SchedulerException {
        Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5).atZone(ZoneId.systemDefault()).toInstant());

        JobDetail jobDetail = JobBuilder.newJob(SimpleJob.class).usingJobData(RETRY_INITIAL_INTERVAL_SECS_KEY, 30).build();
        Trigger trigger = TriggerBuilder.newTrigger().startAt(afterFiveSeconds).usingJobData("", "").build();
        scheduler.scheduleJob(jobDetail, trigger);
    }
}

Заключение

Quartz — это надежная платформа с широким спектром возможностей. Даже если чего-то не хватает, всегда можно реализовать необходимый функционал с помощью предоставленного API. Это позволяет разработчикам расширять платформу в соответствии со своими конкретными требованиями, поэтому Quartz — отличный выбор для любых задач планирования.


Оригинал