From 279589a5dfe1737e58f075cac32959691df768e0 Mon Sep 17 00:00:00 2001 From: Martin Rojas Miguel Angel Date: Thu, 13 Sep 2018 11:32:37 +0200 Subject: [PATCH] TSK-681 Create workbasket cleanup job --- .../java/pro/taskana/WorkbasketQuery.java | 11 +- .../java/pro/taskana/WorkbasketService.java | 13 ++ .../TaskanaEngineConfiguration.java | 48 +++--- .../pro/taskana/impl/WorkbasketQueryImpl.java | 16 +- .../taskana/impl/WorkbasketServiceImpl.java | 72 +++++++- .../pro/taskana/jobs/AbstractTaskanaJob.java | 2 + .../jobs/ClassificationChangedJob.java | 2 +- .../main/java/pro/taskana/jobs/JobRunner.java | 22 +-- .../java/pro/taskana/jobs/ScheduledJob.java | 3 +- .../java/pro/taskana/jobs/TaskCleanupJob.java | 22 +-- .../taskana/jobs/WorkbasketCleanupJob.java | 162 ++++++++++++++++++ .../pro/taskana/mappings/QueryMapper.java | 1 + .../taskana/mappings/WorkbasketMapper.java | 22 ++- .../jobs/WorkbasketCleanupJobAccTest.java | 82 +++++++++ .../java/pro/taskana/jobs/JobScheduler.java | 1 + 15 files changed, 408 insertions(+), 71 deletions(-) create mode 100644 lib/taskana-core/src/main/java/pro/taskana/jobs/WorkbasketCleanupJob.java create mode 100644 lib/taskana-core/src/test/java/acceptance/jobs/WorkbasketCleanupJobAccTest.java diff --git a/lib/taskana-core/src/main/java/pro/taskana/WorkbasketQuery.java b/lib/taskana-core/src/main/java/pro/taskana/WorkbasketQuery.java index 4fc1911a5..3c7b8e1f9 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/WorkbasketQuery.java +++ b/lib/taskana-core/src/main/java/pro/taskana/WorkbasketQuery.java @@ -20,7 +20,6 @@ public interface WorkbasketQuery extends BaseQuery { /** * Add your keys to your query. The keys are compared case-insensitively to the keys of workbaskets with the IN * operator. - * * @param key * the keys as Strings * @return the query @@ -482,4 +481,14 @@ public interface WorkbasketQuery extends BaseQuery { * @return the query */ WorkbasketQuery orgLevel4Like(String... orgLevel4); + + /** + * Add to your query if the Workbasket shall be marked for deletion. + * + * @param deletionFlag + * a simple flag showing if deletion flag is activated + * @return the query + */ + WorkbasketQuery deletionFlagEquals(Boolean deletionFlag); + } diff --git a/lib/taskana-core/src/main/java/pro/taskana/WorkbasketService.java b/lib/taskana-core/src/main/java/pro/taskana/WorkbasketService.java index fface7d41..8f1cf6298 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/WorkbasketService.java +++ b/lib/taskana-core/src/main/java/pro/taskana/WorkbasketService.java @@ -6,6 +6,7 @@ import pro.taskana.exceptions.DomainNotFoundException; import pro.taskana.exceptions.InvalidArgumentException; import pro.taskana.exceptions.InvalidWorkbasketException; import pro.taskana.exceptions.NotAuthorizedException; +import pro.taskana.exceptions.TaskanaException; import pro.taskana.exceptions.WorkbasketAlreadyExistException; import pro.taskana.exceptions.WorkbasketInUseException; import pro.taskana.exceptions.WorkbasketNotFoundException; @@ -319,6 +320,18 @@ public interface WorkbasketService { boolean deleteWorkbasket(String workbasketId) throws NotAuthorizedException, WorkbasketNotFoundException, WorkbasketInUseException, InvalidArgumentException; + /** + * Deletes a list of workbaskets. + * + * @param workbasketsIds + * the ids of the workbaskets to delete. + * @return the result of the operations with Id and Exception for each failed workbasket deletion. + * @throws InvalidArgumentException + * if the WorkbasketId parameter is NULL + */ + BulkOperationResults deleteWorkbaskets(List workbasketsIds) + throws NotAuthorizedException, WorkbasketNotFoundException, WorkbasketInUseException, InvalidArgumentException; + /** * Returns the distribution sources for a given workbasket. * diff --git a/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java index e0b46062e..a249c4956 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/configuration/TaskanaEngineConfiguration.java @@ -48,9 +48,9 @@ public class TaskanaEngineConfiguration { private static final String TASKANA_ROLES_SEPARATOR = "|"; private static final String TASKANA_JOB_BATCHSIZE = "taskana.jobs.batchSize"; private static final String TASKANA_JOB_RETRIES = "taskana.jobs.maxRetries"; - private static final String TASKANA_JOB_TASK_CLEANUP_RUN_EVERY = "taskana.jobs.cleanup.runEvery"; - private static final String TASKANA_JOB_TASK_CLEANUP_FIRST_RUN = "taskana.jobs.cleanup.firstRunAt"; - private static final String TASKANA_JOB_TASK_CLEANUP_MINIMUM_AGE = "taskana.jobs.cleanup.minimumAge"; + private static final String TASKANA_JOB_CLEANUP_RUN_EVERY = "taskana.jobs.cleanup.runEvery"; + private static final String TASKANA_JOB_CLEANUP_FIRST_RUN = "taskana.jobs.cleanup.firstRunAt"; + private static final String TASKANA_JOB_CLEANUP_MINIMUM_AGE = "taskana.jobs.cleanup.minimumAge"; private static final String TASKANA_JOB_TASK_CLEANUP_ALL_COMPLETED_SAME_PARENTE_BUSINESS = "taskana.jobs.cleanup.allCompletedSameParentBusiness"; private static final String TASKANA_DOMAINS_PROPERTY = "taskana.domains"; @@ -86,9 +86,9 @@ public class TaskanaEngineConfiguration { private int maxNumberOfJobRetries = 3; // Properties for the cleanup job - private Instant taskCleanupJobFirstRun = Instant.parse("2018-01-01T00:00:00Z"); - private Duration taskCleanupJobRunEvery = Duration.parse("P1D"); - private Duration taskCleanupJobMinimumAge = Duration.parse("P14D"); + private Instant cleanupJobFirstRun = Instant.parse("2018-01-01T00:00:00Z"); + private Duration cleanupJobRunEvery = Duration.parse("P1D"); + private Duration cleanupJobMinimumAge = Duration.parse("P14D"); private boolean taskCleanupJobAllCompletedSameParentBusiness = true; // List of configured domain names @@ -174,30 +174,30 @@ public class TaskanaEngineConfiguration { } } - String taskCleanupJobFirstRunProperty = props.getProperty(TASKANA_JOB_TASK_CLEANUP_FIRST_RUN); + String taskCleanupJobFirstRunProperty = props.getProperty(TASKANA_JOB_CLEANUP_FIRST_RUN); if (taskCleanupJobFirstRunProperty != null && !taskCleanupJobFirstRunProperty.isEmpty()) { try { - taskCleanupJobFirstRun = Instant.parse(taskCleanupJobFirstRunProperty); + cleanupJobFirstRun = Instant.parse(taskCleanupJobFirstRunProperty); } catch (Exception e) { LOGGER.warn("Could not parse taskCleanupJobFirstRunProperty ({}). Using default. Exception: {} ", taskCleanupJobFirstRunProperty, e.getMessage()); } } - String taskCleanupJobRunEveryProperty = props.getProperty(TASKANA_JOB_TASK_CLEANUP_RUN_EVERY); + String taskCleanupJobRunEveryProperty = props.getProperty(TASKANA_JOB_CLEANUP_RUN_EVERY); if (taskCleanupJobRunEveryProperty != null && !taskCleanupJobRunEveryProperty.isEmpty()) { try { - taskCleanupJobRunEvery = Duration.parse(taskCleanupJobRunEveryProperty); + cleanupJobRunEvery = Duration.parse(taskCleanupJobRunEveryProperty); } catch (Exception e) { LOGGER.warn("Could not parse taskCleanupJobRunEveryProperty ({}). Using default. Exception: {} ", taskCleanupJobRunEveryProperty, e.getMessage()); } } - String taskCleanupJobMinimumAgeProperty = props.getProperty(TASKANA_JOB_TASK_CLEANUP_MINIMUM_AGE); + String taskCleanupJobMinimumAgeProperty = props.getProperty(TASKANA_JOB_CLEANUP_MINIMUM_AGE); if (taskCleanupJobMinimumAgeProperty != null && !taskCleanupJobMinimumAgeProperty.isEmpty()) { try { - taskCleanupJobMinimumAge = Duration.parse(taskCleanupJobMinimumAgeProperty); + cleanupJobMinimumAge = Duration.parse(taskCleanupJobMinimumAgeProperty); } catch (Exception e) { LOGGER.warn("Could not parse taskCleanupJobMinimumAgeProperty ({}). Using default. Exception: {} ", taskCleanupJobMinimumAgeProperty, e.getMessage()); @@ -218,12 +218,12 @@ public class TaskanaEngineConfiguration { } } - LOGGER.debug("Configured number of task updates per transaction: {}", jobBatchSize); + LOGGER.debug("Configured number of task and workbasket updates per transaction: {}", jobBatchSize); LOGGER.debug("Number of retries of failed task updates: {}", maxNumberOfJobRetries); - LOGGER.debug("TaskCleanupJob configuration: first run at {}", taskCleanupJobFirstRun); - LOGGER.debug("TaskCleanupJob configuration: runs every {}", taskCleanupJobRunEvery); - LOGGER.debug("TaskCleanupJob configuration: minimum age of tasks to be cleanup up is {}", - taskCleanupJobMinimumAge); + LOGGER.debug("CleanupJob configuration: first run at {}", cleanupJobFirstRun); + LOGGER.debug("CleanupJob configuration: runs every {}", cleanupJobRunEvery); + LOGGER.debug("CleanupJob configuration: minimum age of tasks to be cleanup up is {}", + cleanupJobMinimumAge); LOGGER.debug("TaskCleanupJob configuration: all completed task with the same parent business property id {}", taskCleanupJobAllCompletedSameParentBusiness); } @@ -399,7 +399,7 @@ public class TaskanaEngineConfiguration { return this.propertiesFileName; } - public int getMaxNumberOfTaskUpdatesPerTransaction() { + public int getMaxNumberOfUpdatesPerTransaction() { return jobBatchSize; } @@ -475,16 +475,16 @@ public class TaskanaEngineConfiguration { this.classificationCategoriesByTypeMap = classificationCategoriesByType; } - public Instant getTaskCleanupJobFirstRun() { - return taskCleanupJobFirstRun; + public Instant getCleanupJobFirstRun() { + return cleanupJobFirstRun; } - public Duration getTaskCleanupJobRunEvery() { - return taskCleanupJobRunEvery; + public Duration getCleanupJobRunEvery() { + return cleanupJobRunEvery; } - public Duration getTaskCleanupJobMinimumAge() { - return taskCleanupJobMinimumAge; + public Duration getCleanupJobMinimumAge() { + return cleanupJobMinimumAge; } public void setTaskCleanupJobAllCompletedSameParentBusiness(boolean taskCleanupJobAllCompletedSameParentBusiness) { diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketQueryImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketQueryImpl.java index cb61ef67b..6da208180 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketQueryImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketQueryImpl.java @@ -67,6 +67,8 @@ public class WorkbasketQueryImpl implements WorkbasketQuery { private String[] orgLevel3Like; private String[] orgLevel4In; private String[] orgLevel4Like; + private boolean isDeletionFlagActivated; + private TaskanaEngineImpl taskanaEngine; private List orderBy; private List orderColumns; @@ -272,6 +274,12 @@ public class WorkbasketQueryImpl implements WorkbasketQuery { return this; } + @Override + public WorkbasketQuery deletionFlagEquals(Boolean deletionFlag) { + this.isDeletionFlagActivated = deletionFlag; + return this; + } + @Override public WorkbasketQuery orderByName(SortDirection sortDirection) { return addOrderCriteria("NAME", sortDirection); @@ -396,7 +404,7 @@ public class WorkbasketQueryImpl implements WorkbasketQuery { this.columnName = columnName; handleCallerRolesAndAccessIds(); this.orderBy.clear(); - this.addOrderCriteria(columnName, sortDirection); + //this.addOrderCriteria(columnName, sortDirection); result = taskanaEngine.getSqlSession().selectList(LINK_TO_VALUEMAPPER, this); return result; } finally { @@ -587,6 +595,10 @@ public class WorkbasketQueryImpl implements WorkbasketQuery { return orgLevel4Like; } + public boolean isDeletionFlagActivated() { + return isDeletionFlagActivated; + } + public String[] getOwnerLike() { return ownerLike; } @@ -688,6 +700,8 @@ public class WorkbasketQueryImpl implements WorkbasketQuery { builder.append(Arrays.toString(orgLevel4In)); builder.append(", orgLevel4Like="); builder.append(Arrays.toString(orgLevel4Like)); + builder.append(", deletionFlag="); + builder.append(isDeletionFlagActivated); builder.append(", orderBy="); builder.append(orderBy); builder.append(", joinWithAccessList="); diff --git a/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketServiceImpl.java b/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketServiceImpl.java index f50ecd999..f931906fb 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketServiceImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/impl/WorkbasketServiceImpl.java @@ -3,12 +3,13 @@ package pro.taskana.impl; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; +import java.util.Iterator; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import pro.taskana.BulkOperationResults; import pro.taskana.TaskState; import pro.taskana.TaskanaEngine; import pro.taskana.TaskanaRole; @@ -23,6 +24,7 @@ import pro.taskana.exceptions.DomainNotFoundException; import pro.taskana.exceptions.InvalidArgumentException; import pro.taskana.exceptions.InvalidWorkbasketException; import pro.taskana.exceptions.NotAuthorizedException; +import pro.taskana.exceptions.TaskanaException; import pro.taskana.exceptions.WorkbasketAlreadyExistException; import pro.taskana.exceptions.WorkbasketInUseException; import pro.taskana.exceptions.WorkbasketNotFoundException; @@ -725,7 +727,6 @@ public class WorkbasketServiceImpl implements WorkbasketService { throws NotAuthorizedException, WorkbasketNotFoundException, WorkbasketInUseException, InvalidArgumentException { LOGGER.debug("entry to deleteWorkbasket(workbasketId = {})", workbasketId); taskanaEngine.checkRoleMembership(TaskanaRole.BUSINESS_ADMIN, TaskanaRole.ADMIN); - HashMap response = new HashMap(); try { taskanaEngine.openConnection(); if (workbasketId == null || workbasketId.isEmpty()) { @@ -779,19 +780,80 @@ public class WorkbasketServiceImpl implements WorkbasketService { WorkbasketImpl workbasket = workbasketMapper.findById(workbasketId); workbasket.setMarkedForDeletion(true); workbasketMapper.update(workbasket); - distributionTargetMapper.deleteAllDistributionTargetsBySourceId(workbasketId); - distributionTargetMapper.deleteAllDistributionTargetsByTargetId(workbasketId); - workbasketAccessMapper.deleteAllAccessItemsForWorkbasketId(workbasketId); + deleteWorkbasketTablesReferences(workbasketId); } finally { taskanaEngine.returnConnection(); LOGGER.debug("exit from markWorkbasketForDeletion(workbasketId = {}).", workbasketId); } } + private void deleteWorkbasketTablesReferences(String workbasketId) { + // delete workbasket and sub-tables + distributionTargetMapper.deleteAllDistributionTargetsBySourceId(workbasketId); + distributionTargetMapper.deleteAllDistributionTargetsByTargetId(workbasketId); + workbasketAccessMapper.deleteAllAccessItemsForWorkbasketId(workbasketId); + } + + public BulkOperationResults deleteWorkbaskets(List workbasketsIds) + throws NotAuthorizedException, InvalidArgumentException, WorkbasketInUseException, WorkbasketNotFoundException { + LOGGER.debug("entry to deleteWorkbaskets(workbasketId = {})", LoggerUtils.listToString(workbasketsIds)); + + taskanaEngine.checkRoleMembership(TaskanaRole.BUSINESS_ADMIN, TaskanaRole.ADMIN); + + try { + taskanaEngine.openConnection(); + if (workbasketsIds == null || workbasketsIds.isEmpty()) { + throw new InvalidArgumentException("List of WorkbasketIds must not be null."); + } + + List existingWorkbasketIds = workbasketMapper.findExistingWorkbaskets(workbasketsIds); + BulkOperationResults bulkLog = cleanNonExistingWorkbasketExists( + existingWorkbasketIds, + workbasketsIds); + + if (!existingWorkbasketIds.isEmpty()) { + Iterator iterator = existingWorkbasketIds.iterator(); + while (iterator.hasNext()) { + deleteWorkbasket(iterator.next()); + } + } + return bulkLog; + } finally { + LOGGER.debug("exit from deleteWorkbaskets()"); + taskanaEngine.returnConnection(); + } + } + @Override public WorkbasketAccessItemQuery createWorkbasketAccessItemQuery() throws NotAuthorizedException { taskanaEngine.checkRoleMembership(TaskanaRole.ADMIN, TaskanaRole.BUSINESS_ADMIN); return new WorkbasketAccessItemQueryImpl(this.taskanaEngine); } + private BulkOperationResults cleanNonExistingWorkbasketExists( + List existingWorkbasketIds, + List workbasketIds) { + BulkOperationResults bulkLog = new BulkOperationResults<>(); + Iterator workbasketIdIterator = existingWorkbasketIds.iterator(); + while (workbasketIdIterator.hasNext()) { + String currentWorkbasketId = workbasketIdIterator.next(); + if (currentWorkbasketId == null || currentWorkbasketId.equals("")) { + bulkLog.addError("", + new InvalidArgumentException("IDs with EMPTY or NULL value are not allowed.")); + workbasketIdIterator.remove(); + } else { + String foundSummary = workbasketIds.stream() + .filter(workbasketId -> currentWorkbasketId.equals(workbasketId)) + .findFirst() + .orElse(null); + if (foundSummary == null) { + bulkLog.addError(currentWorkbasketId, new WorkbasketNotFoundException(currentWorkbasketId, + "Workbasket with id " + currentWorkbasketId + " was not found.")); + workbasketIdIterator.remove(); + } + } + } + return bulkLog; + } + } diff --git a/lib/taskana-core/src/main/java/pro/taskana/jobs/AbstractTaskanaJob.java b/lib/taskana-core/src/main/java/pro/taskana/jobs/AbstractTaskanaJob.java index 485c9dfac..2fab25250 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/jobs/AbstractTaskanaJob.java +++ b/lib/taskana-core/src/main/java/pro/taskana/jobs/AbstractTaskanaJob.java @@ -34,6 +34,8 @@ public abstract class AbstractTaskanaJob implements TaskanaJob { return new TaskRefreshJob(engine, txProvider, job); case TASKCLEANUPJOB: return new TaskCleanupJob(engine, txProvider, job); + case WORKBASKETCLEANUPJOB: + return new WorkbasketCleanupJob(engine, txProvider, job); default: throw new TaskanaException( "No matching job found for " + job.getType() + " of ScheduledJob " + job.getJobId() + "."); diff --git a/lib/taskana-core/src/main/java/pro/taskana/jobs/ClassificationChangedJob.java b/lib/taskana-core/src/main/java/pro/taskana/jobs/ClassificationChangedJob.java index 1489caa0a..bfbaf5bb9 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/jobs/ClassificationChangedJob.java +++ b/lib/taskana-core/src/main/java/pro/taskana/jobs/ClassificationChangedJob.java @@ -82,7 +82,7 @@ public class ClassificationChangedJob extends AbstractTaskanaJob { } private void scheduleTaskRefreshJobs(Set affectedTaskIds) { - int batchSize = taskanaEngineImpl.getConfiguration().getMaxNumberOfTaskUpdatesPerTransaction(); + int batchSize = taskanaEngineImpl.getConfiguration().getMaxNumberOfUpdatesPerTransaction(); List> affectedTaskBatches = partition(affectedTaskIds, batchSize); LOGGER.debug("Creating {} TaskRefreshJobs out of {} affected tasks with a maximum number of {} tasks each. ", affectedTaskBatches.size(), affectedTaskIds.size(), batchSize); diff --git a/lib/taskana-core/src/main/java/pro/taskana/jobs/JobRunner.java b/lib/taskana-core/src/main/java/pro/taskana/jobs/JobRunner.java index 5edf19182..154d35a75 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/jobs/JobRunner.java +++ b/lib/taskana-core/src/main/java/pro/taskana/jobs/JobRunner.java @@ -63,11 +63,9 @@ public class JobRunner { } private ScheduledJob lockJobTransactionally(ScheduledJob job) { - ScheduledJob lockedJob = null; + ScheduledJob lockedJob; if (txProvider != null) { - lockedJob = (ScheduledJob) txProvider.executeInTransaction(() -> { - return lockJob(job); - }); + lockedJob = (ScheduledJob) txProvider.executeInTransaction(() -> lockJob(job)); } else { lockedJob = lockJob(job); } @@ -101,25 +99,9 @@ public class JobRunner { jobService.deleteJob(scheduledJob); } catch (Exception e) { e.printStackTrace(); - // transaction was rolled back -> split job into 2 half sized jobs LOGGER.warn( "Processing of job " + scheduledJob.getJobId() + " failed. Trying to split it up into two pieces...", e); - // rescheduleBisectedJob(bulkLog, job); - // List objectIds; - // if (job.getType().equals(ScheduledJob.Type.UPDATETASKSJOB)) { - // String taskIdsAsString = job.getArguments().get(SingleJobExecutor.TASKIDS); - // objectIds = Arrays.asList(taskIdsAsString.split(",")); - // } else if (job.getType().equals(ScheduledJob.Type.CLASSIFICATIONCHANGEDJOB)) { - // String classificationId = job.getArguments().get(SingleJobExecutor.CLASSIFICATION_ID); - // objectIds = Arrays.asList(classificationId); - // } else { - // throw new SystemException("Unknown Jobtype " + job.getType() + " encountered."); - // } - // for (String objectId : objectIds) { - // bulkLog.addError(objectId, e); - // } - // setJobFailed(job, bulkLog); } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/jobs/ScheduledJob.java b/lib/taskana-core/src/main/java/pro/taskana/jobs/ScheduledJob.java index 712967c4d..5038b4436 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/jobs/ScheduledJob.java +++ b/lib/taskana-core/src/main/java/pro/taskana/jobs/ScheduledJob.java @@ -150,6 +150,7 @@ public class ScheduledJob { public enum Type { CLASSIFICATIONCHANGEDJOB, UPDATETASKSJOB, - TASKCLEANUPJOB; + TASKCLEANUPJOB, + WORKBASKETCLEANUPJOB; } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/jobs/TaskCleanupJob.java b/lib/taskana-core/src/main/java/pro/taskana/jobs/TaskCleanupJob.java index 2346dfea1..53202a0e0 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/jobs/TaskCleanupJob.java +++ b/lib/taskana-core/src/main/java/pro/taskana/jobs/TaskCleanupJob.java @@ -39,10 +39,10 @@ public class TaskCleanupJob extends AbstractTaskanaJob { public TaskCleanupJob(TaskanaEngine taskanaEngine, TaskanaTransactionProvider txProvider, ScheduledJob scheduledJob) { super(taskanaEngine, txProvider, scheduledJob); - firstRun = taskanaEngine.getConfiguration().getTaskCleanupJobFirstRun(); - runEvery = taskanaEngine.getConfiguration().getTaskCleanupJobRunEvery(); - minimumAge = taskanaEngine.getConfiguration().getTaskCleanupJobMinimumAge(); - batchSize = taskanaEngine.getConfiguration().getMaxNumberOfTaskUpdatesPerTransaction(); + firstRun = taskanaEngine.getConfiguration().getCleanupJobFirstRun(); + runEvery = taskanaEngine.getConfiguration().getCleanupJobRunEvery(); + minimumAge = taskanaEngine.getConfiguration().getCleanupJobMinimumAge(); + batchSize = taskanaEngine.getConfiguration().getMaxNumberOfUpdatesPerTransaction(); allCompletedSameParentBusiness = taskanaEngine.getConfiguration() .isTaskCleanupJobAllCompletedSameParentBusiness(); } @@ -62,10 +62,11 @@ public class TaskCleanupJob extends AbstractTaskanaJob { totalNumberOfTasksCompleted += deleteTasksTransactionally(tasksCompletedBefore.subList(0, upperLimit)); tasksCompletedBefore.subList(0, upperLimit).clear(); } - scheduleNextCleanupJob(); LOGGER.info("Job ended successfully. {} tasks deleted.", totalNumberOfTasksCompleted); } catch (Exception e) { throw new TaskanaException("Error while processing TaskCleanupJob.", e); + } finally { + scheduleNextCleanupJob(); } } @@ -80,11 +81,10 @@ public class TaskCleanupJob extends AbstractTaskanaJob { Map numberParentTasksShouldHave = new HashMap<>(); Map countParentTask = new HashMap<>(); for (TaskSummary task : taskList) { - numberParentTasksShouldHave.put(task.getParentBusinessProcessId(), - taskanaEngineImpl.getTaskService() - .createTaskQuery() - .parentBusinessProcessIdIn(task.getParentBusinessProcessId()) - .count()); + numberParentTasksShouldHave.put(task.getParentBusinessProcessId(), taskanaEngineImpl.getTaskService() + .createTaskQuery() + .parentBusinessProcessIdIn(task.getParentBusinessProcessId()) + .count()); countParentTask.merge(task.getParentBusinessProcessId(), 1L, Long::sum); } @@ -145,7 +145,7 @@ public class TaskCleanupJob extends AbstractTaskanaJob { return tasksIdsToBeDeleted.size() - results.getFailedIds().size(); } - public void scheduleNextCleanupJob() { + private void scheduleNextCleanupJob() { LOGGER.debug("Entry to scheduleNextCleanupJob."); ScheduledJob job = new ScheduledJob(); job.setType(ScheduledJob.Type.TASKCLEANUPJOB); diff --git a/lib/taskana-core/src/main/java/pro/taskana/jobs/WorkbasketCleanupJob.java b/lib/taskana-core/src/main/java/pro/taskana/jobs/WorkbasketCleanupJob.java new file mode 100644 index 000000000..8b2e663d7 --- /dev/null +++ b/lib/taskana-core/src/main/java/pro/taskana/jobs/WorkbasketCleanupJob.java @@ -0,0 +1,162 @@ +package pro.taskana.jobs; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import pro.taskana.BaseQuery; +import pro.taskana.BulkOperationResults; +import pro.taskana.TaskService; +import pro.taskana.TaskanaEngine; +import pro.taskana.exceptions.InvalidArgumentException; +import pro.taskana.exceptions.NotAuthorizedException; +import pro.taskana.exceptions.TaskanaException; +import pro.taskana.exceptions.WorkbasketInUseException; +import pro.taskana.exceptions.WorkbasketNotFoundException; +import pro.taskana.impl.util.LoggerUtils; +import pro.taskana.transaction.TaskanaTransactionProvider; + +/** + * Job to cleanup completed workbaskets after a period of time if there are no pending tasks associated to the workbasket. + */ +public class WorkbasketCleanupJob extends AbstractTaskanaJob { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskCleanupJob.class); + + // Parameter + private Instant firstRun; + private Duration runEvery; + private int batchSize; + + public WorkbasketCleanupJob(TaskanaEngine taskanaEngine, + TaskanaTransactionProvider txProvider, ScheduledJob job) { + super(taskanaEngine, txProvider, job); + firstRun = taskanaEngine.getConfiguration().getCleanupJobFirstRun(); + runEvery = taskanaEngine.getConfiguration().getCleanupJobRunEvery(); + batchSize = taskanaEngine.getConfiguration().getMaxNumberOfUpdatesPerTransaction(); + } + + @Override + public void run() throws TaskanaException { + LOGGER.info("Running job to delete all workbaskets marked for deletion"); + try { + List workbasketsMarkedForDeletion = getWorkbasketsMarkedForDeletion(); + int totalNumberOfWorkbasketDeleted = 0; + while (workbasketsMarkedForDeletion.size() > 0) { + int upperLimit = batchSize; + if (upperLimit > workbasketsMarkedForDeletion.size()) { + upperLimit = workbasketsMarkedForDeletion.size(); + } + totalNumberOfWorkbasketDeleted += deleteWorkbasketsTransactionally( + workbasketsMarkedForDeletion.subList(0, upperLimit)); + workbasketsMarkedForDeletion.subList(0, upperLimit).clear(); + } + LOGGER.info("Job ended successfully. {} workbaskets deleted.", totalNumberOfWorkbasketDeleted); + } catch (Exception e) { + throw new TaskanaException("Error while processing WorkbasketCleanupJob.", e); + } finally { + scheduleNextCleanupJob(); + } + } + + private List getWorkbasketsMarkedForDeletion() throws InvalidArgumentException { + List workbasketList = taskanaEngineImpl.getWorkbasketService() + .createWorkbasketQuery() + .deletionFlagEquals(true) + .listValues("ID", BaseQuery.SortDirection.ASCENDING); + workbasketList = excludeWorkbasketWithPendingTasks(workbasketList); + + return workbasketList; + } + + private List excludeWorkbasketWithPendingTasks(List workbasketList) + throws InvalidArgumentException { + TaskService taskService = taskanaEngineImpl.getTaskService(); + ArrayList workbasketDeletionList = new ArrayList<>(); + ArrayList workbasketWithNonCompletedTasksList = new ArrayList<>(); + + if (!workbasketList.isEmpty()) { + Iterator iterator = workbasketList.iterator(); + while (iterator.hasNext()) { + String workbasketId = iterator.next(); + if (taskService.allTasksCompletedByWorkbasketId(workbasketId)) { + workbasketDeletionList.add(workbasketId); + } else { + workbasketWithNonCompletedTasksList.add(workbasketId); + } + } + } + LOGGER.info("workbasket marked for deletion with non completed tasks {}.", + LoggerUtils.listToString(workbasketWithNonCompletedTasksList)); + return workbasketDeletionList; + } + + private int deleteWorkbasketsTransactionally(List workbasketsToBeDeleted) { + int deletedWorkbasketsCount = 0; + if (txProvider != null) { + Integer count = (Integer) txProvider.executeInTransaction(() -> { + try { + return new Integer(deleteWorkbaskets(workbasketsToBeDeleted)); + } catch (Exception e) { + LOGGER.warn("Could not delete workbaskets.", e); + return new Integer(0); + } + }); + return count.intValue(); + } else { + try { + deletedWorkbasketsCount = deleteWorkbaskets(workbasketsToBeDeleted); + } catch (Exception e) { + LOGGER.warn("Could not delete workbaskets.", e); + } + } + return deletedWorkbasketsCount; + } + + private int deleteWorkbaskets(List workbasketsToBeDeleted) + throws InvalidArgumentException, NotAuthorizedException, WorkbasketNotFoundException, WorkbasketInUseException { + + BulkOperationResults results = taskanaEngineImpl.getWorkbasketService() + .deleteWorkbaskets(workbasketsToBeDeleted); + LOGGER.debug("{} workbasket deleted.", workbasketsToBeDeleted.size() - results.getFailedIds().size()); + for (String failedId : results.getFailedIds()) { + LOGGER.warn("Workbasket with id {} could not be deleted. Reason: {}", failedId, + results.getErrorForId(failedId)); + } + return workbasketsToBeDeleted.size() - results.getFailedIds().size(); + } + + private void scheduleNextCleanupJob() { + LOGGER.debug("Entry to scheduleNextCleanupJob."); + ScheduledJob job = new ScheduledJob(); + job.setType(ScheduledJob.Type.WORKBASKETCLEANUPJOB); + job.setDue(getNextDueForWorkbasketCleanupJob()); + taskanaEngineImpl.getJobService().createJob(job); + LOGGER.debug("Exit from scheduleNextCleanupJob."); + } + + private Instant getNextDueForWorkbasketCleanupJob() { + Instant nextRunAt = firstRun; + while (nextRunAt.isBefore(Instant.now())) { + nextRunAt = nextRunAt.plus(runEvery); + } + LOGGER.info("Scheduling next run of the WorkbasketCleanupJob for {}", nextRunAt); + return nextRunAt; + } + + /** + * Initializes the WorkbasketCleanupJob schedule.
+ * All scheduled cleanup jobs are cancelled/deleted and a new one is scheduled. + * + * @param taskanaEngine + */ + public static void initializeSchedule(TaskanaEngine taskanaEngine) { + WorkbasketCleanupJob job = new WorkbasketCleanupJob(taskanaEngine, null, null); + job.scheduleNextCleanupJob(); + } +} diff --git a/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java b/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java index f8bcbe3f0..73c98733a 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java +++ b/lib/taskana-core/src/main/java/pro/taskana/mappings/QueryMapper.java @@ -1167,6 +1167,7 @@ public interface QueryMapper { + "AND (UPPER(w.ORG_LEVEL_3) LIKE #{item}) " + "AND w.ORG_LEVEL_4 IN(#{item}) " + "AND (UPPER(w.ORG_LEVEL_4) LIKE #{item}) " + + "AND w.DELETION_FLAG = #{isDeletionFlagActivated} " + " " + " " + "AND (a.MAX_READ = 1 " diff --git a/lib/taskana-core/src/main/java/pro/taskana/mappings/WorkbasketMapper.java b/lib/taskana-core/src/main/java/pro/taskana/mappings/WorkbasketMapper.java index 26db4b08a..2e3d84e70 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/mappings/WorkbasketMapper.java +++ b/lib/taskana-core/src/main/java/pro/taskana/mappings/WorkbasketMapper.java @@ -65,9 +65,10 @@ public interface WorkbasketMapper { @Result(property = "markedForDeletion", column = "MARKED_FOR_DELETION")}) WorkbasketImpl findByKeyAndDomain(@Param("key") String key, @Param("domain") String domain); - @Select("") + @Select( + "") @Results(value = { @Result(property = "id", column = "ID"), @Result(property = "key", column = "KEY"), @@ -86,10 +87,11 @@ public interface WorkbasketMapper { @Result(property = "orgLevel4", column = "ORG_LEVEL_4")}) List findDistributionTargets(@Param("id") String id); - @Select("") + @Select( + "") @Results(value = { @Result(property = "id", column = "ID"), @Result(property = "key", column = "KEY"), @@ -150,6 +152,12 @@ public interface WorkbasketMapper { @Result(property = "orgLevel4", column = "ORG_LEVEL_4")}) List findAll(); + @Select("") + List findExistingWorkbaskets(@Param("workbasketIds") List workbasketIds); + @Insert("") @Options(keyProperty = "id", keyColumn = "ID") diff --git a/lib/taskana-core/src/test/java/acceptance/jobs/WorkbasketCleanupJobAccTest.java b/lib/taskana-core/src/test/java/acceptance/jobs/WorkbasketCleanupJobAccTest.java new file mode 100644 index 000000000..7826ddcbd --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/jobs/WorkbasketCleanupJobAccTest.java @@ -0,0 +1,82 @@ +package acceptance.jobs; + +import static org.junit.Assert.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import acceptance.AbstractAccTest; +import pro.taskana.BaseQuery; +import pro.taskana.TaskService; +import pro.taskana.WorkbasketService; +import pro.taskana.WorkbasketSummary; +import pro.taskana.jobs.WorkbasketCleanupJob; +import pro.taskana.security.JAASRunner; +import pro.taskana.security.WithAccessId; + +import java.util.List; + +/** + * Acceptance test for all "jobs workbasket runner" scenarios. + */ +@RunWith(JAASRunner.class) +public class WorkbasketCleanupJobAccTest extends AbstractAccTest { + + WorkbasketService workbasketService; + TaskService taskService; + + @Before + public void before() { + workbasketService = taskanaEngine.getWorkbasketService(); + taskService = taskanaEngine.getTaskService(); + } + + @After + public void after() throws Exception { + resetDb(true); + } + + @WithAccessId(userName = "admin") + @Test + public void shouldCleanWorkbasketMarkedForDeletion() throws Exception { + long totalWorkbasketCount = workbasketService.createWorkbasketQuery().count(); + assertEquals(25, totalWorkbasketCount); + List workbaskets = workbasketService.createWorkbasketQuery() + .keyIn("GPK_KSC", "sort001") + .orderByKey( + BaseQuery.SortDirection.ASCENDING) + .list(); + assertEquals(taskService.allTasksCompletedByWorkbasketId(workbaskets.get(1).getId()), true); + workbasketService.markWorkbasketForDeletion(workbaskets.get(1).getId()); + + WorkbasketCleanupJob job = new WorkbasketCleanupJob(taskanaEngine, null, null); + job.run(); + + totalWorkbasketCount = workbasketService.createWorkbasketQuery().count(); + assertEquals(24, totalWorkbasketCount); + } + + @WithAccessId(userName = "admin") + @Test + public void shouldCleanWorkbasketMarkedForDeletionWithCompletedTasks() throws Exception { + long totalWorkbasketCount = workbasketService.createWorkbasketQuery().count(); + assertEquals(25, totalWorkbasketCount); + List workbaskets = workbasketService.createWorkbasketQuery() + .keyIn("GPK_KSC", "sort001") + .orderByKey( + BaseQuery.SortDirection.ASCENDING) + .list(); + assertEquals(taskService.allTasksCompletedByWorkbasketId(workbaskets.get(0).getId()), false); + assertEquals(taskService.allTasksCompletedByWorkbasketId(workbaskets.get(1).getId()), true); + workbasketService.markWorkbasketForDeletion(workbaskets.get(0).getId()); + workbasketService.markWorkbasketForDeletion(workbaskets.get(1).getId()); + + WorkbasketCleanupJob job = new WorkbasketCleanupJob(taskanaEngine, null, null); + job.run(); + + totalWorkbasketCount = workbasketService.createWorkbasketQuery().count(); + assertEquals(24, totalWorkbasketCount); + } +} diff --git a/rest/taskana-rest-spring-example/src/main/java/pro/taskana/jobs/JobScheduler.java b/rest/taskana-rest-spring-example/src/main/java/pro/taskana/jobs/JobScheduler.java index 404b54145..1d9197358 100644 --- a/rest/taskana-rest-spring-example/src/main/java/pro/taskana/jobs/JobScheduler.java +++ b/rest/taskana-rest-spring-example/src/main/java/pro/taskana/jobs/JobScheduler.java @@ -38,6 +38,7 @@ public class JobScheduler { public void scheduleCleanupJob() { LOGGER.debug("Entry to scheduleCleanupJob."); TaskCleanupJob.initializeSchedule(taskanaEngine); + WorkbasketCleanupJob.initializeSchedule(taskanaEngine); LOGGER.debug("Exit from scheduleCleanupJob."); }