Повторное повторение кварца при отказе
Скажем, у меня есть триггер, сконфигурированный таким образом:
<bean id="updateInsBBTrigger"
class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="updateInsBBJobDetail"/>
<!-- run every morning at 5 AM -->
<property name="cronExpression" value="0 0 5 * * ?"/>
</bean>
Триггер должен подключиться к другому приложению, и если возникнут какие-либо проблемы (например, сбой подключения), он должен повторить задачу до пяти раз каждые 10 минут или до достижения успеха. Есть ли способ настроить триггер для работы следующим образом?
Ответы
Ответ 1
Источник: Автоматическое повторение неудачных заданий в кварце
Если вы хотите иметь задание, которое пытается повторить попытку снова и снова до тех пор, пока оно не удастся, все, что вам нужно сделать, это бросить исключение JobExecutionException с флагом, чтобы сообщить планировщику, чтобы он снова запускал его, когда он терпел неудачу. Следующий код показывает, как:
class MyJob implements Job {
public MyJob() {
}
public void execute(JobExecutionContext context) throws JobExecutionException {
try{
//connect to other application etc
}
catch(Exception e){
Thread.sleep(600000); //sleep for 10 mins
JobExecutionException e2 = new JobExecutionException(e);
//fire it again
e2.setRefireImmediately(true);
throw e2;
}
}
}
Это немного сложнее, если вы хотите повторить определенное количество раз. Вы должны использовать StatefulJob и удерживать retryCounter в своем JobDataMap, который вы увеличиваете, если задание терпит неудачу. Если счетчик превышает максимальное количество попыток, вы можете отключить задание, если хотите.
class MyJob implements StatefulJob {
public MyJob() {
}
public void execute(JobExecutionContext context) throws JobExecutionException {
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
int count = dataMap.getIntValue("count");
// allow 5 retries
if(count >= 5){
JobExecutionException e = new JobExecutionException("Retries exceeded");
//make sure it doesn't run again
e.setUnscheduleAllTriggers(true);
throw e;
}
try{
//connect to other application etc
//reset counter back to 0
dataMap.putAsString("count", 0);
}
catch(Exception e){
count++;
dataMap.putAsString("count", count);
JobExecutionException e2 = new JobExecutionException(e);
Thread.sleep(600000); //sleep for 10 mins
//fire it again
e2.setRefireImmediately(true);
throw e2;
}
}
}
Ответ 2
Я бы предложил для большей гибкости и конфигурации, чтобы лучше хранить в вашем DB два смещения: repeatOffset, который скажет вам
после того, как долго должно быть повторено задание и пробный пакетPeriodOffset, который будет хранить информацию о временном окне, что работа
разрешено перепланировать. Затем вы можете получить эти два параметра, например (я предполагаю, что вы используете Spring):
String repeatOffset = yourDBUtilsDao.getConfigParameter(..);
String trialPeriodOffset = yourDBUtilsDao.getConfigParameter(..);
Затем вместо задания, чтобы помнить счетчик, необходимо запомнить initalAttempt:
Long initialAttempt = null;
initialAttempt = (Long) existingJobDetail.getJobDataMap().get("firstAttempt");
и выполните что-то вроде следующей проверки:
long allowedThreshold = initialAttempt + Long.parseLong(trialPeriodOffset);
if (System.currentTimeMillis() > allowedThreshold) {
//We've tried enough, time to give up
log.warn("The job is not going to be rescheduled since it has reached its trial period threshold");
sched.deleteJob(jobName, jobGroup);
return YourResultEnumHere.HAS_REACHED_THE_RESCHEDULING_LIMIT;
}
Было бы неплохо создать перечисление для результата попытки, возвращаемой обратно в основной рабочий процесс вашего
например, выше.
Затем постройте время перепланирования:
Date startTime = null;
startTime = new Date(System.currentTimeMillis() + Long.parseLong(repeatOffset));
String triggerName = "Trigger_" + jobName;
String triggerGroup = "Trigger_" + jobGroup;
Trigger retrievedTrigger = sched.getTrigger(triggerName, triggerGroup);
if (!(retrievedTrigger instanceof SimpleTrigger)) {
log.error("While rescheduling the Quartz Job retrieved was not of SimpleTrigger type as expected");
return YourResultEnumHere.ERROR;
}
((SimpleTrigger) retrievedTrigger).setStartTime(startTime);
sched.rescheduleJob(triggerName, triggerGroup, retrievedTrigger);
return YourResultEnumHere.RESCHEDULED;
Ответ 3
Я бы рекомендовал реализацию, подобную этой, чтобы восстановить задание после сбоя:
final JobDataMap jobDataMap = jobCtx.getJobDetail().getJobDataMap();
// the keys doesn't exist on first retry
final int retries = jobDataMap.containsKey(COUNT_MAP_KEY) ? jobDataMap.getIntValue(COUNT_MAP_KEY) : 0;
// to stop after awhile
if (retries < MAX_RETRIES) {
log.warn("Retry job " + jobCtx.getJobDetail());
// increment the number of retries
jobDataMap.put(COUNT_MAP_KEY, retries + 1);
final JobDetail job = jobCtx
.getJobDetail()
.getJobBuilder()
// to track the number of retries
.withIdentity(jobCtx.getJobDetail().getKey().getName() + " - " + retries, "FailingJobsGroup")
.usingJobData(jobDataMap)
.build();
final OperableTrigger trigger = (OperableTrigger) TriggerBuilder
.newTrigger()
.forJob(job)
// trying to reduce back pressure, you can use another algorithm
.startAt(new Date(jobCtx.getFireTime().getTime() + (retries*100)))
.build();
try {
// schedule another job to avoid blocking threads
jobCtx.getScheduler().scheduleJob(job, trigger);
} catch (SchedulerException e) {
log.error("Error creating job");
throw new JobExecutionException(e);
}
}
Почему?
- Он не будет блокировать рабочих кварца
- Это позволит избежать противодавления. С setRefireImiately работа будет немедленно запущена, и это может привести к проблемам с противодавлением.