TSK-1686: Introduced an SPI for the individual calculation of priorities

This commit is contained in:
Tim Gerversmann 2021-08-16 17:19:52 +02:00 committed by Mustapha Zorgati
parent 1c5456337b
commit dc9e3a25ce
9 changed files with 275 additions and 2 deletions

View File

@ -37,6 +37,7 @@ import pro.taskana.spi.history.api.events.classification.ClassificationCreatedEv
import pro.taskana.spi.history.api.events.classification.ClassificationDeletedEvent; import pro.taskana.spi.history.api.events.classification.ClassificationDeletedEvent;
import pro.taskana.spi.history.api.events.classification.ClassificationUpdatedEvent; import pro.taskana.spi.history.api.events.classification.ClassificationUpdatedEvent;
import pro.taskana.spi.history.internal.HistoryEventManager; import pro.taskana.spi.history.internal.HistoryEventManager;
import pro.taskana.spi.priority.internal.PriorityServiceManager;
import pro.taskana.task.api.models.TaskSummary; import pro.taskana.task.api.models.TaskSummary;
import pro.taskana.task.internal.TaskMapper; import pro.taskana.task.internal.TaskMapper;
@ -253,7 +254,10 @@ public class ClassificationServiceImpl implements ClassificationService {
this.checkExistenceOfParentClassification(oldClassification, classificationImpl); this.checkExistenceOfParentClassification(oldClassification, classificationImpl);
classificationMapper.update(classificationImpl); classificationMapper.update(classificationImpl);
this.createJobIfPriorityOrServiceLevelHasChanged(oldClassification, classificationImpl);
if (!PriorityServiceManager.isPriorityServiceEnabled()) {
this.createJobIfPriorityOrServiceLevelHasChanged(oldClassification, classificationImpl);
}
if (HistoryEventManager.isHistoryEnabled()) { if (HistoryEventManager.isHistoryEnabled()) {
String details = String details =

View File

@ -5,6 +5,7 @@ import org.apache.ibatis.session.SqlSession;
import pro.taskana.common.api.TaskanaEngine; import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.spi.history.internal.HistoryEventManager; import pro.taskana.spi.history.internal.HistoryEventManager;
import pro.taskana.spi.priority.internal.PriorityServiceManager;
import pro.taskana.spi.routing.internal.TaskRoutingManager; import pro.taskana.spi.routing.internal.TaskRoutingManager;
import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager;
@ -97,4 +98,11 @@ public interface InternalTaskanaEngine {
* @return the CreateTaskPreprocessorManager instance. * @return the CreateTaskPreprocessorManager instance.
*/ */
CreateTaskPreprocessorManager getCreateTaskPreprocessorManager(); CreateTaskPreprocessorManager getCreateTaskPreprocessorManager();
/**
* Retrieves the {@linkplain PriorityServiceManager}.
*
* @return the {@linkplain PriorityServiceManager} instance
*/
PriorityServiceManager getPriorityServiceManager();
} }

View File

@ -51,6 +51,7 @@ import pro.taskana.monitor.api.MonitorService;
import pro.taskana.monitor.internal.MonitorMapper; import pro.taskana.monitor.internal.MonitorMapper;
import pro.taskana.monitor.internal.MonitorServiceImpl; import pro.taskana.monitor.internal.MonitorServiceImpl;
import pro.taskana.spi.history.internal.HistoryEventManager; import pro.taskana.spi.history.internal.HistoryEventManager;
import pro.taskana.spi.priority.internal.PriorityServiceManager;
import pro.taskana.spi.routing.internal.TaskRoutingManager; import pro.taskana.spi.routing.internal.TaskRoutingManager;
import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager;
import pro.taskana.task.api.TaskService; import pro.taskana.task.api.TaskService;
@ -76,6 +77,7 @@ public class TaskanaEngineImpl implements TaskanaEngine {
private static final SessionStack SESSION_STACK = new SessionStack(); private static final SessionStack SESSION_STACK = new SessionStack();
private final TaskRoutingManager taskRoutingManager; private final TaskRoutingManager taskRoutingManager;
private final CreateTaskPreprocessorManager createTaskPreprocessorManager; private final CreateTaskPreprocessorManager createTaskPreprocessorManager;
private final PriorityServiceManager priorityServiceManager;
private final InternalTaskanaEngineImpl internalTaskanaEngineImpl; private final InternalTaskanaEngineImpl internalTaskanaEngineImpl;
private final WorkingDaysToDaysConverter workingDaysToDaysConverter; private final WorkingDaysToDaysConverter workingDaysToDaysConverter;
private final HistoryEventManager historyEventManager; private final HistoryEventManager historyEventManager;
@ -106,6 +108,7 @@ public class TaskanaEngineImpl implements TaskanaEngine {
// to provide a fully initialized TaskanaEngine instance during the SPI initialization! // to provide a fully initialized TaskanaEngine instance during the SPI initialization!
historyEventManager = HistoryEventManager.getInstance(this); historyEventManager = HistoryEventManager.getInstance(this);
taskRoutingManager = TaskRoutingManager.getInstance(this); taskRoutingManager = TaskRoutingManager.getInstance(this);
priorityServiceManager = PriorityServiceManager.getInstance();
} }
public static TaskanaEngine createTaskanaEngine( public static TaskanaEngine createTaskanaEngine(
@ -472,5 +475,10 @@ public class TaskanaEngineImpl implements TaskanaEngine {
public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() { public CreateTaskPreprocessorManager getCreateTaskPreprocessorManager() {
return createTaskPreprocessorManager; return createTaskPreprocessorManager;
} }
@Override
public PriorityServiceManager getPriorityServiceManager() {
return priorityServiceManager;
}
} }
} }

View File

@ -0,0 +1,25 @@
package pro.taskana.spi.priority.api;
import java.util.Optional;
import pro.taskana.task.api.models.Task;
import pro.taskana.task.api.models.TaskSummary;
/**
* This SPI enables the computation of {@linkplain Task} priorities depending on individual
* preferences.
*/
public interface PriorityServiceProvider {
/**
* Calculates the {@linkplain Task#getPriority() priority} of a certain {@linkplain Task}.
*
* <p>The implemented method must calculate the {@linkplain Task#getPriority() priority}
* efficiently. There can be a huge amount of {@linkplain Task Tasks} the SPI has to handle.
*
* @param taskSummary the {@linkplain TaskSummary} to compute the {@linkplain Task#getPriority()
* priority} for
* @return the computed {@linkplain Task#getPriority() priority}
*/
Optional<Integer> calculatePriority(TaskSummary taskSummary);
}

View File

@ -0,0 +1,84 @@
package pro.taskana.spi.priority.internal;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.internal.util.LogSanitizer;
import pro.taskana.spi.priority.api.PriorityServiceProvider;
import pro.taskana.task.api.models.TaskSummary;
public class PriorityServiceManager {
private static final Logger LOGGER = LoggerFactory.getLogger(PriorityServiceManager.class);
private static PriorityServiceManager singleton;
private final ServiceLoader<PriorityServiceProvider> serviceLoader;
private boolean enabled = false;
private PriorityServiceManager() {
serviceLoader = ServiceLoader.load(PriorityServiceProvider.class);
for (PriorityServiceProvider priorityProvider : serviceLoader) {
LOGGER.info("Registered PriorityServiceProvider: {}", priorityProvider.getClass().getName());
enabled = true;
}
if (!enabled) {
LOGGER.info("No PriorityServiceProvider found. Running without PriorityServiceProvider.");
}
}
public static synchronized PriorityServiceManager getInstance() {
if (singleton == null) {
singleton = new PriorityServiceManager();
}
return singleton;
}
public static boolean isPriorityServiceEnabled() {
return Objects.nonNull(singleton) && singleton.enabled;
}
public Optional<Integer> calculatePriorityOfTask(TaskSummary task) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Sending task to PriorityServiceProviders: {}", task);
}
// ServiceLoader.stream() is only available in Java11.
List<Integer> priorities =
StreamSupport.stream(serviceLoader.spliterator(), false)
.map(provider -> getPriorityByProvider(task, provider))
.filter(Optional::isPresent)
.map(Optional::get)
.distinct()
.collect(Collectors.toList());
if (priorities.size() <= 1) {
return priorities.stream().findFirst();
}
if (LOGGER.isErrorEnabled()) {
LOGGER.error(
"The PriorityServiceProviders determined more than one priority for Task {}.",
LogSanitizer.stripLineBreakingChars(task));
}
return Optional.empty();
}
private Optional<Integer> getPriorityByProvider(
TaskSummary task, PriorityServiceProvider provider) {
try {
return provider.calculatePriority(task);
} catch (Exception e) {
throw new SystemException(
String.format(
"Caught exception while calculating priority of Task in provider %s.",
provider.getClass().getName()),
e);
}
}
}

View File

@ -47,6 +47,7 @@ import pro.taskana.spi.history.api.events.task.TaskCreatedEvent;
import pro.taskana.spi.history.api.events.task.TaskTerminatedEvent; import pro.taskana.spi.history.api.events.task.TaskTerminatedEvent;
import pro.taskana.spi.history.api.events.task.TaskUpdatedEvent; import pro.taskana.spi.history.api.events.task.TaskUpdatedEvent;
import pro.taskana.spi.history.internal.HistoryEventManager; import pro.taskana.spi.history.internal.HistoryEventManager;
import pro.taskana.spi.priority.internal.PriorityServiceManager;
import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager; import pro.taskana.spi.task.internal.CreateTaskPreprocessorManager;
import pro.taskana.task.api.CallbackState; import pro.taskana.task.api.CallbackState;
import pro.taskana.task.api.TaskCustomField; import pro.taskana.task.api.TaskCustomField;
@ -99,6 +100,7 @@ public class TaskServiceImpl implements TaskService {
private final AttachmentMapper attachmentMapper; private final AttachmentMapper attachmentMapper;
private final HistoryEventManager historyEventManager; private final HistoryEventManager historyEventManager;
private final CreateTaskPreprocessorManager createTaskPreprocessorManager; private final CreateTaskPreprocessorManager createTaskPreprocessorManager;
private final PriorityServiceManager priorityServiceManager;
public TaskServiceImpl( public TaskServiceImpl(
InternalTaskanaEngine taskanaEngine, InternalTaskanaEngine taskanaEngine,
@ -112,6 +114,7 @@ public class TaskServiceImpl implements TaskService {
this.classificationService = taskanaEngine.getEngine().getClassificationService(); this.classificationService = taskanaEngine.getEngine().getClassificationService();
this.historyEventManager = taskanaEngine.getHistoryEventManager(); this.historyEventManager = taskanaEngine.getHistoryEventManager();
this.createTaskPreprocessorManager = taskanaEngine.getCreateTaskPreprocessorManager(); this.createTaskPreprocessorManager = taskanaEngine.getCreateTaskPreprocessorManager();
this.priorityServiceManager = taskanaEngine.getPriorityServiceManager();
this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this); this.taskTransferrer = new TaskTransferrer(taskanaEngine, taskMapper, this);
this.taskCommentService = new TaskCommentServiceImpl(taskanaEngine, taskCommentMapper, this); this.taskCommentService = new TaskCommentServiceImpl(taskanaEngine, taskCommentMapper, this);
this.serviceLevelHandler = this.serviceLevelHandler =
@ -222,6 +225,14 @@ public class TaskServiceImpl implements TaskService {
ObjectReference.validate(task.getPrimaryObjRef(), "primary ObjectReference", "Task"); ObjectReference.validate(task.getPrimaryObjRef(), "primary ObjectReference", "Task");
standardSettingsOnTaskCreation(task, classification); standardSettingsOnTaskCreation(task, classification);
setCallbackStateOnTaskCreation(task); setCallbackStateOnTaskCreation(task);
if (PriorityServiceManager.isPriorityServiceEnabled()) {
Optional<Integer> newPriority = priorityServiceManager.calculatePriorityOfTask(task);
if (newPriority.isPresent()) {
task.setPriority(newPriority.get());
}
}
try { try {
this.taskMapper.insert(task); this.taskMapper.insert(task);
if (LOGGER.isDebugEnabled()) { if (LOGGER.isDebugEnabled()) {
@ -419,6 +430,13 @@ public class TaskServiceImpl implements TaskService {
standardUpdateActions(oldTaskImpl, newTaskImpl); standardUpdateActions(oldTaskImpl, newTaskImpl);
if (PriorityServiceManager.isPriorityServiceEnabled()) {
Optional<Integer> newPriority = priorityServiceManager.calculatePriorityOfTask(newTaskImpl);
if (newPriority.isPresent()) {
newTaskImpl.setPriority(newPriority.get());
}
}
taskMapper.update(newTaskImpl); taskMapper.update(newTaskImpl);
if (LOGGER.isDebugEnabled()) { if (LOGGER.isDebugEnabled()) {

View File

@ -71,7 +71,9 @@ class ArchitectureTest {
"pro.taskana.spi.routing.api", "pro.taskana.spi.routing.api",
"pro.taskana.spi.routing.internal", "pro.taskana.spi.routing.internal",
"pro.taskana.spi.task.api", "pro.taskana.spi.task.api",
"pro.taskana.spi.task.internal"); "pro.taskana.spi.task.internal",
"pro.taskana.spi.priority.api",
"pro.taskana.spi.priority.internal");
private static JavaClasses importedClasses; private static JavaClasses importedClasses;
@BeforeAll @BeforeAll

View File

@ -0,0 +1,97 @@
package acceptance.priorityservice;
import static org.assertj.core.api.Assertions.assertThat;
import acceptance.AbstractAccTest;
import java.sql.Date;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.function.ThrowingConsumer;
import pro.taskana.classification.api.ClassificationService;
import pro.taskana.classification.api.models.Classification;
import pro.taskana.common.api.ScheduledJob;
import pro.taskana.common.internal.util.Pair;
import pro.taskana.common.test.security.JaasExtension;
import pro.taskana.common.test.security.WithAccessId;
import pro.taskana.task.api.TaskCustomField;
import pro.taskana.task.api.TaskService;
import pro.taskana.task.api.models.ObjectReference;
import pro.taskana.task.api.models.Task;
/** Acceptance test for all priority computation scenarios. */
@Disabled("Until we enable the use of Test-SPI's in only specific tests")
@ExtendWith(JaasExtension.class)
class PriorityServiceAccTest extends AbstractAccTest {
private static final TaskService TASK_SERVICE = taskanaEngine.getTaskService();
@WithAccessId(user = "user-1-1")
@TestFactory
Stream<DynamicTest> should_SetThePriorityAccordingToTestProvider_When_CreatingTask() {
List<Pair<String, Integer>> testCases = List.of(Pair.of("false", 1), Pair.of("true", 10));
ThrowingConsumer<Pair<String, Integer>> test =
x -> {
Task task = TASK_SERVICE.newTask("USER-1-1", "DOMAIN_A");
task.setCustomAttribute(TaskCustomField.CUSTOM_6, x.getLeft());
task.setClassificationKey("T2100");
ObjectReference objectReference =
createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567");
task.setPrimaryObjRef(objectReference);
Task createdTask = TASK_SERVICE.createTask(task);
assertThat(createdTask.getPriority()).isEqualTo(x.getRight());
};
return DynamicTest.stream(testCases.iterator(), x -> "entry in custom6: " + x.getLeft(), test);
}
@WithAccessId(user = "user-1-1")
@TestFactory
Stream<DynamicTest> should_SetThePriorityAccordingToTestProvider_When_UpdatingTask()
throws Exception {
List<Pair<String, Integer>> testCases = List.of(Pair.of("false", 1), Pair.of("true", 10));
Task task = TASK_SERVICE.getTask("TKI:000000000000000000000000000000000000");
int daysSinceCreated =
Math.toIntExact(
TimeUnit.DAYS.convert(
Date.from(Instant.now()).getTime() - Date.from(task.getCreated()).getTime(),
TimeUnit.MILLISECONDS));
ThrowingConsumer<Pair<String, Integer>> test =
x -> {
task.setCustomAttribute(TaskCustomField.CUSTOM_6, x.getLeft());
Task updatedTask = TASK_SERVICE.updateTask(task);
assertThat(updatedTask.getPriority()).isEqualTo(daysSinceCreated * x.getRight());
};
return DynamicTest.stream(testCases.iterator(), x -> "entry in custom6: " + x.getLeft(), test);
}
@WithAccessId(user = "admin")
@Test
void should_NotCreateClassificationChangedJob_When_PriorityProviderExisting() throws Exception {
ClassificationService classificationService = taskanaEngine.getClassificationService();
Classification classification =
classificationService.getClassification("CLI:000000000000000000000000000000000001");
classification.setPriority(10);
classificationService.updateClassification(classification);
List<ScheduledJob> jobsToRun = getJobMapper().findJobsToRun(Instant.now());
assertThat(jobsToRun).isEmpty();
classification.setServiceLevel("P4D");
classificationService.updateClassification(classification);
jobsToRun = getJobMapper().findJobsToRun(Instant.now());
assertThat(jobsToRun).isEmpty();
}
}

View File

@ -0,0 +1,27 @@
package acceptance.priorityservice;
import java.sql.Date;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import pro.taskana.spi.priority.api.PriorityServiceProvider;
import pro.taskana.task.api.TaskCustomField;
import pro.taskana.task.api.models.TaskSummary;
public class TestPriorityServiceProvider implements PriorityServiceProvider {
private static final int MULTIPLIER = 10;
@Override
public Optional<Integer> calculatePriority(TaskSummary taskSummary) {
long diffInMillies =
Date.from(Instant.now()).getTime() - Date.from(taskSummary.getCreated()).getTime();
long diffInDays = TimeUnit.DAYS.convert(diffInMillies, TimeUnit.MILLISECONDS);
int priority = diffInDays >= 1 ? Math.toIntExact(diffInDays) : 1;
if (taskSummary.getCustomAttribute(TaskCustomField.CUSTOM_6) == "true") {
priority *= MULTIPLIER;
}
return Optional.of(Integer.valueOf(priority));
}
}