/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.provisionr.core.activiti;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.UUID;
import org.activiti.engine.impl.calendar.BusinessCalendar;
import org.activiti.engine.impl.calendar.CycleBusinessCalendar;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.jobexecutor.FailedJobCommandFactory;
import org.activiti.engine.impl.persistence.entity.JobEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* We need a custom @{link FailedJobCommandFactory} implementation that allows
* us to customize the global number of retries per job
*/
public class ConfigurableFailedJobCommandFactory implements FailedJobCommandFactory {
private static final Logger LOG = LoggerFactory.getLogger(ConfigurableFailedJobCommandFactory.class);
/**
* This class is a bit of a hack because there is no easy way to
* change @{link JobEntity.DEFAULT_RETRIES}
* <p/>
* We get the desired behaviour by incrementing the number of retries until we
* exceed maxNumberOfRetries + DEFAULT_RETRIES (considered to be 0)
* <p/>
* If maxNumberOfRetries is -1 (infinity) the job will be always retried.
*/
public static class IncrementJobRetriesCmd implements Command<Object> {
private final String LOCK_OWNER = "job-retries-" + UUID.randomUUID().toString();
private final String jobId;
private final Throwable exception;
private final int maxNumberOfRetries;
private final int waitBetweenRetriesInSeconds;
public IncrementJobRetriesCmd(String jobId, Throwable exception, int maxNumberOfRetries,
int waitBetweenRetriesInSeconds) {
this.jobId = checkNotNull(jobId, "jobId is null");
this.exception = exception;
this.maxNumberOfRetries = maxNumberOfRetries;
this.waitBetweenRetriesInSeconds = waitBetweenRetriesInSeconds;
}
@Override
public Object execute(CommandContext commandContext) {
JobEntity job = Context.getCommandContext()
.getJobManager()
.findJobById(jobId);
updateNumberOfRetries(job);
if (exception != null) {
job.setExceptionMessage(exception.getMessage());
job.setExceptionStacktrace(getExceptionStacktrace());
}
return null;
}
private void updateNumberOfRetries(JobEntity job) {
if (maxNumberOfRetries == -1) {
job.setLockOwner(LOCK_OWNER);
job.setLockExpirationTime(calculateDueDate());
return; /* always retry without counting */
}
int realUpperBound = maxNumberOfRetries + JobEntity.DEFAULT_RETRIES;
if (job.getRetries() >= realUpperBound) {
LOG.warn("Job {} from process {} has no more retries left. The process will block and may " +
"require human intervention.", job.getId(), job.getProcessInstanceId());
job.setRetries(0); /* stop retrying this job */
job.setLockOwner(null);
} else {
final Date newDate = calculateDueDate();
LOG.info("Scheduling job {} from process {} to be retried at {}. Try {}/{}",
new Object[]{job.getId(), job.getProcessInstanceId(), newDate,
job.getRetries() - JobEntity.DEFAULT_RETRIES, maxNumberOfRetries});
/* I've tried to use job.setDuedate() but it doesn't work as expected */
job.setLockOwner(LOCK_OWNER);
job.setLockExpirationTime(newDate);
job.setRetries(job.getRetries() + 1);
}
}
/**
* Based on code from @{link TimerEntity.calculateDueDate}
*/
private Date calculateDueDate() {
BusinessCalendar businessCalendar = Context
.getProcessEngineConfiguration()
.getBusinessCalendarManager()
.getBusinessCalendar(CycleBusinessCalendar.NAME);
return businessCalendar.resolveDuedate(
String.format("R/PT%dS", waitBetweenRetriesInSeconds));
}
private String getExceptionStacktrace() {
StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}
}
private final int maxNumberOfRetries;
private final int waitBetweenRetriesInSeconds;
public ConfigurableFailedJobCommandFactory(int maxNumberOfRetries, int waitBetweenRetriesInSeconds) {
checkArgument(maxNumberOfRetries > 0 || maxNumberOfRetries == -1,
"Max number of retries should be a positive number or -1 (infinite)");
checkArgument(waitBetweenRetriesInSeconds > 0, "waitBetweenRetriesInSeconds should be positive");
this.maxNumberOfRetries = maxNumberOfRetries;
this.waitBetweenRetriesInSeconds = waitBetweenRetriesInSeconds;
}
@Override
public Command<Object> getCommand(String jobId, Throwable exception) {
return new IncrementJobRetriesCmd(jobId, exception, maxNumberOfRetries, waitBetweenRetriesInSeconds);
}
}