package fr.openwide.core.jpa.more.business.task.model;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import fr.openwide.core.commons.util.CloneUtils;
import fr.openwide.core.jpa.exception.SecurityServiceException;
import fr.openwide.core.jpa.exception.ServiceException;
import fr.openwide.core.jpa.more.business.task.service.IQueuedTaskHolderService;
import fr.openwide.core.jpa.more.business.task.transaction.OpenEntityManagerWithNoTransactionTransactionTemplate;
import fr.openwide.core.jpa.more.business.task.transaction.TaskExecutionTransactionTemplateConfig;
import fr.openwide.core.jpa.more.business.task.util.TaskResult;
import fr.openwide.core.jpa.more.business.task.util.TaskStatus;
import fr.openwide.core.jpa.more.config.spring.AbstractTaskManagementConfig;
import fr.openwide.core.jpa.util.EntityManagerUtils;
public abstract class AbstractTask implements Runnable, Serializable {
private static final long serialVersionUID = 7734300264023051135L;
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractTask.class);
@Autowired
protected IQueuedTaskHolderService queuedTaskHolderService;
@Autowired
@Qualifier(AbstractTaskManagementConfig.OBJECT_MAPPER_BEAN_NAME)
private ObjectMapper queuedTaskHolderObjectMapper;
@JsonIgnore
private TransactionTemplate taskManagementTransactionTemplate;
@JsonIgnore
private TransactionTemplate taskExecutionTransactionTemplate;
@JsonIgnore
protected Long queuedTaskHolderId;
protected Date triggeringDate;
protected String taskName;
protected String taskType;
@JsonIgnore
private TaskExecutionResult taskExecutionResult;
protected AbstractTask() { }
public AbstractTask(String taskName, ITaskTypeProvider taskTypeProvider, Date triggeringDate) {
this(taskName, taskTypeProvider.getTaskType(), triggeringDate);
}
public AbstractTask(String taskName, String taskType, Date triggeringDate) {
setTaskName(taskName);
setTaskType(taskType);
setTriggeringDate(triggeringDate);
}
/**
* @return The ID of the queue this task must be added in, or <code>null</code> for the default queue.
*/
public IQueueId selectQueue() {
return null;
}
@Autowired
private void setTransactionManager(EntityManagerUtils entityManagerUtils, PlatformTransactionManager transactionManager) {
// Le TransactionTemplate pour la gestion du cycle de vie doit forcément être défini comme cela,
// pas besoin de pouvoir le redéfinir.
DefaultTransactionAttribute defaultTransactionAttributes = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
taskManagementTransactionTemplate = new TransactionTemplate(transactionManager, defaultTransactionAttributes);
// On donne la main sur l'initialisation du TransactionTemplate pour l'exécution de la tâche.
taskExecutionTransactionTemplate = newTaskExecutionTransactionTemplate(
entityManagerUtils, transactionManager);
}
/**
* Permet d'initialiser le TransactionManager utilisé pour l'exécution de la tâche.
* Dans la mesure du possible, surcharger plutôt {@link #getTaskExecutionTransactionTemplateConfig()}.
*/
protected TransactionTemplate newTaskExecutionTransactionTemplate(EntityManagerUtils entityManagerUtils,
PlatformTransactionManager transactionManager) {
TaskExecutionTransactionTemplateConfig config = getTaskExecutionTransactionTemplateConfig();
TransactionTemplate taskExecutionTransactionTemplate;
if (config.isTransactional()) {
DefaultTransactionAttribute defaultTransactionAttributes = new DefaultTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
defaultTransactionAttributes.setReadOnly(config.isReadOnly());
taskExecutionTransactionTemplate = new TransactionTemplate(transactionManager, defaultTransactionAttributes);
} else {
taskExecutionTransactionTemplate = new OpenEntityManagerWithNoTransactionTransactionTemplate(
entityManagerUtils, transactionManager, config.isReadOnly());
}
return taskExecutionTransactionTemplate;
}
/**
* Permet de configurer le TransactionManager utilisé pour l'exécution de la tâche.
*/
protected TaskExecutionTransactionTemplateConfig getTaskExecutionTransactionTemplateConfig() {
return new TaskExecutionTransactionTemplateConfig();
}
/**
* This method is final. If you need to customize its behavior, check out one of the onXX() methods.
* @see #onBeforeStart()
* @see #onBeforeExecute()
* @see #onAfterExecute(TaskExecutionResult, TaskStatus)
*/
@Override
public final void run() {
/*
* Stores the "this thread was interrupted" information.
* For some reason, the Thread.isInterrupted() flag is not properly preserved after using
* a transaction template. Maybe some code does a Thread.sleep, then catches InterruptedException but never
* resets the flag. Anyway, using this mutable boolean works around the problem.
*/
final MutableBoolean interruptedFlag = new MutableBoolean(false);
try {
onBeforeStart();
taskExecutionResult = taskManagementTransactionTemplate.execute(new TransactionCallback<TaskExecutionResult>() {
@Override
public TaskExecutionResult doInTransaction(TransactionStatus status) {
try {
QueuedTaskHolder queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId);
if (queuedTaskHolder == null) {
throw new IllegalArgumentException("No task found with id " + queuedTaskHolderId);
}
queuedTaskHolder.setStartDate(new Date());
queuedTaskHolder.setStatus(TaskStatus.RUNNING);
queuedTaskHolderService.update(queuedTaskHolder);
return null;
} catch (RuntimeException | ServiceException | SecurityServiceException e) {
status.setRollbackOnly();
return TaskExecutionResult.failed(e);
} finally {
if (Thread.currentThread().isInterrupted()) {
// Save the information (seems to be cleared by the transaction template)
interruptedFlag.setTrue();
}
}
}
});
if (taskExecutionResult != null && TaskResult.FATAL.equals(taskExecutionResult.getResult())) {
final TaskStatus status = taskManagementTransactionTemplate.execute(new TransactionCallback<TaskStatus>() {
@Override
public TaskStatus doInTransaction(TransactionStatus status) {
try {
QueuedTaskHolder queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId);
final TaskStatus taskStatus;
if (queuedTaskHolder == null) {
LOGGER.error("Task {} could not be found; skipped execution.", queuedTaskHolderId, taskExecutionResult);
return null;
}
if (interruptedFlag.isTrue()) {
LOGGER.error("An interrupt has occured while starting task {}", queuedTaskHolder, taskExecutionResult.getThrowable());
taskStatus = onInterruptStatus(taskExecutionResult);
} else {
LOGGER.error("An error has occured while starting task {}", queuedTaskHolder, taskExecutionResult.getThrowable());
taskStatus = onFailStatus(taskExecutionResult);
}
endTask(queuedTaskHolder, taskStatus);
return taskStatus;
} catch (RuntimeException | JsonProcessingException | ServiceException | SecurityServiceException e) {
throw new RuntimeException(e);
}
}
});
// If we could not switch to "RUNNING", stop right now.
onAfterRun(status);
return;
}
onBeforeExecute();
taskExecutionResult = taskExecutionTransactionTemplate.execute(new TransactionCallback<TaskExecutionResult>() {
@Override
public TaskExecutionResult doInTransaction(TransactionStatus status) {
try {
/*
* The result may contain a business exception, caught in doTask.
* We don't have to propagate it, the task will be considered in error and onFailStatus()
* will be called.
*/
TaskExecutionResult executionResult = doTask();
Objects.requireNonNull(executionResult, "executionResult must not be null");
/*
* If the task execution caught a business exception in doTask without propagating it
* (so as to preserve a batch report), we roll back the transaction.
*/
if (TaskResult.FATAL.equals(executionResult.getResult())) {
status.setRollbackOnly();
}
return executionResult;
} catch (InterruptedException e) {
status.setRollbackOnly();
Thread.currentThread().interrupt();
return TaskExecutionResult.failed(e);
} catch (Exception e) {
status.setRollbackOnly();
return TaskExecutionResult.failed(e);
} finally {
if (Thread.currentThread().isInterrupted()) {
// Save the information (seems to be cleared by the transaction template)
interruptedFlag.setTrue();
}
}
}
});
// Should not happen, but just in case...
if (taskExecutionResult == null) {
throw new RuntimeException();
}
final TaskStatus status;
if (!TaskResult.FATAL.equals(taskExecutionResult.getResult())) {
// Success case
status = taskManagementTransactionTemplate.execute(new TransactionCallback<TaskStatus>() {
@Override
public TaskStatus doInTransaction(TransactionStatus status) {
try {
QueuedTaskHolder queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId);
endTask(queuedTaskHolder, TaskStatus.COMPLETED);
return TaskStatus.COMPLETED;
} catch (RuntimeException | JsonProcessingException | ServiceException | SecurityServiceException e) {
throw new RuntimeException(e);
}
}
});
} else {
// Error case
status = taskManagementTransactionTemplate.execute(new TransactionCallback<TaskStatus>() {
@Override
public TaskStatus doInTransaction(TransactionStatus status) {
try {
QueuedTaskHolder queuedTaskHolder = queuedTaskHolderService.getById(queuedTaskHolderId);
final TaskStatus taskStatus;
if (interruptedFlag.isTrue()) {
LOGGER.error("An interrupt has occured while executing task {}", queuedTaskHolder, taskExecutionResult.getThrowable());
taskStatus = onInterruptStatus(taskExecutionResult);
} else {
LOGGER.error("An error has occured while executing task {}", queuedTaskHolder, taskExecutionResult.getThrowable());
taskStatus = onFailStatus(taskExecutionResult);
}
endTask(queuedTaskHolder, taskStatus);
return taskStatus;
} catch (RuntimeException | JsonProcessingException | ServiceException | SecurityServiceException e) {
throw new RuntimeException(e);
}
}
});
}
onAfterExecute(taskExecutionResult, status);
onAfterRun(status);
taskExecutionResult = null;
} finally {
if (interruptedFlag.isTrue()) {
Thread.currentThread().interrupt();
}
}
}
/**
* Called before starting a task, i.e. before marking it "RUNNING".
* <p>This method is not executed in a transaction. If the implementor requires one, he should use
* {@link #getTaskExecutionTransactionTemplate()} or {@link #getTaskManagementTransactionTemplate()}.
*/
protected void onBeforeStart() {
// Default: do nothing. Override this if necessary.
}
/**
* Called before executing a task, i.e. when it's marked <code>RUNNING</code>, but <code>doTask()</code>
* hasn't been called yet.
* <p>This method is not executed in a transaction. If the implementor requires one, he should use
* {@link #getTaskExecutionTransactionTemplate()} or {@link #getTaskManagementTransactionTemplate()}.
*/
protected void onBeforeExecute() {
// Default: do nothing. Override this if necessary.
}
/**
* Called after having executed a task, i.e. when <code>doTask()</code> has been called and the task has been marked
* with its post-execution status.
* <p><strong>WARNING:</strong> This method is <strong>not</strong> called if the task could not be started.
* <p>Altering <code>result</code> will have no effect on the persisted {@link QueuedTaskHolder}.
* <p>This method is not executed in a transaction. If the implementor requires one, he should use
* {@link #getTaskExecutionTransactionTemplate()} or {@link #getTaskManagementTransactionTemplate()}.
* @see #onAfterRun(TaskStatus)
*/
protected void onAfterExecute(TaskExecutionResult result, TaskStatus status) {
// Default: do nothing. Override this if necessary.
}
/**
* Called after having executed <code>run()</code> and after having the task marked with the given status.
* <p>This method is executed either when a task failed to start, or when the task has actually been executed. It is
* always executed after all other <code>onXX()</code> methods.
* <p>This method is not executed in a transaction. If the implementor requires one, he should use
* {@link #getTaskExecutionTransactionTemplate()} or {@link #getTaskManagementTransactionTemplate()}.
*/
protected void onAfterRun(TaskStatus status) {
// Default: do nothing. Override this if necessary.
}
private void endTask(QueuedTaskHolder queuedTaskHolder, TaskStatus endingStatus)
throws JsonProcessingException, ServiceException, SecurityServiceException {
queuedTaskHolder.setStatus(endingStatus);
queuedTaskHolder.setEndDate(new Date());
queuedTaskHolder.updateExecutionInformation(taskExecutionResult, queuedTaskHolderObjectMapper);
queuedTaskHolderService.update(queuedTaskHolder);
}
/**
* L'exécution de la tâche à proprement parler.
* @return Le résultat de l'exécution de la tâche, obligatoire.
* @throws Exception Exception non gérée par le code métier, perd complètement le BatchReport
*/
protected abstract TaskExecutionResult doTask() throws Exception;
protected TransactionTemplate getTaskManagementTransactionTemplate() {
return taskManagementTransactionTemplate;
}
protected TransactionTemplate getTaskExecutionTransactionTemplate() {
return taskExecutionTransactionTemplate;
}
public Long getQueuedTaskHolderId() {
return queuedTaskHolderId;
}
public void setQueuedTaskHolderId(Long queuedTaskHolderId) {
this.queuedTaskHolderId = queuedTaskHolderId;
}
public Date getTriggeringDate() {
return CloneUtils.clone(triggeringDate);
}
public void setTriggeringDate(Date triggeringDate) {
this.triggeringDate = CloneUtils.clone(triggeringDate);
}
public String getTaskType() {
return taskType;
}
public void setTaskType(String taskType) {
this.taskType = taskType;
}
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
/**
* Permet principalement de définir si une tâche n'ayant pas abouti doit être passée au statut
* CANCELLED (ne se relance pas automatiquement si on redémarre la file) ou FAILED.
*
* @param executionResult permet de choisir le statut en fonction du résultat d'exécution
* (ex : CANCELLED si exception métier, FAILED si exception autre).
*/
@JsonIgnore
public TaskStatus onFailStatus(TaskExecutionResult executionResult) {
return TaskStatus.FAILED;
}
/**
* Permet principalement de définir si une tâche ayant été interrompue doit être passée au statut
* CANCELLED (ne se relance pas automatiquement si on redémarre la file), INTERRUPTED ou FAILED.
*
* @param executionResult permet de choisir le statut en fonction du résultat d'exécution
* (ex : CANCELLED si exception métier, FAILED si exception autre).
*/
@JsonIgnore
public TaskStatus onInterruptStatus(TaskExecutionResult executionResult) {
return TaskStatus.INTERRUPTED;
}
}