diff --git a/lib/taskana-core-test/pom.xml b/lib/taskana-core-test/pom.xml index 527856ccbb..64b7bf9ed4 100644 --- a/lib/taskana-core-test/pom.xml +++ b/lib/taskana-core-test/pom.xml @@ -72,6 +72,12 @@ ${version.equalsverifier} test + + pro.taskana + taskana-common-test + 8.0.2-SNAPSHOT + test + diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java index 9d7608b612..273a6ab7e8 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/task/query/TaskQueryImplAccTest.java @@ -7,11 +7,17 @@ import static pro.taskana.testapi.DefaultTestEntities.defaultTestObjectReference; import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket; +import java.security.PrivilegedAction; import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.security.auth.Subject; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DynamicTest; @@ -26,8 +32,13 @@ import pro.taskana.common.api.IntInterval; import pro.taskana.common.api.KeyDomain; import pro.taskana.common.api.TimeInterval; +import pro.taskana.common.api.exceptions.SystemException; import pro.taskana.common.api.security.CurrentUserContext; +import pro.taskana.common.api.security.UserPrincipal; +import pro.taskana.common.internal.InternalTaskanaEngine; +import pro.taskana.common.internal.util.CheckedConsumer; import pro.taskana.common.internal.util.Pair; +import pro.taskana.common.test.util.ParallelThreadHelper; import pro.taskana.task.api.CallbackState; import pro.taskana.task.api.TaskCustomField; import pro.taskana.task.api.TaskCustomIntField; @@ -54,6 +65,7 @@ class TaskQueryImplAccTest { @TaskanaInject TaskService taskService; + @TaskanaInject InternalTaskanaEngine internalTaskanaEngine; @TaskanaInject WorkbasketService workbasketService; @TaskanaInject CurrentUserContext currentUserContext; @TaskanaInject ClassificationService classificationService; @@ -98,6 +110,91 @@ private void persistPermission(WorkbasketSummary workbasketSummary) throws Excep .buildAndStore(workbasketService, "businessadmin"); } + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class LockResultsEqualsTest { + private static final Integer LOCK_RESULTS_EQUALS = 2; + WorkbasketSummary wb1; + TaskSummary taskSummary1; + TaskSummary taskSummary2; + TaskSummary taskSummary3; + TaskSummary taskSummary4; + + @WithAccessId(user = "user-1-1") + @BeforeAll + void setup() throws Exception { + wb1 = createWorkbasketWithPermission(); + + taskSummary1 = taskInWorkbasket(wb1).state(TaskState.READY) + .buildAndStoreAsSummary(taskService); + taskSummary2 = taskInWorkbasket(wb1).state(TaskState.READY) + .buildAndStoreAsSummary(taskService); + taskSummary3 = + taskInWorkbasket(wb1).state(TaskState.READY) + .buildAndStoreAsSummary(taskService); + taskSummary4 = taskInWorkbasket(wb1).state(TaskState.READY) + .buildAndStoreAsSummary(taskService); + + } + + @Test + void should_ReturnDifferentTasks_For_LockResultsEqualsTwo() throws Exception { + if (System.getenv("DB") != null && (System.getenv("DB").equals("POSTGRES") + || System.getenv("DB").equals("DB2"))) { + + List returnedTasks = Collections.synchronizedList(new ArrayList<>()); + List accessIds = + Collections.synchronizedList( + Stream.of("admin", "admin") + .collect(Collectors.toList())); + + ParallelThreadHelper.runInThread( + getRunnableTest(returnedTasks, accessIds), accessIds.size()); + + assertThat(returnedTasks) + .extracting(TaskSummary::getId) + .containsExactlyInAnyOrder( + taskSummary1.getId(), taskSummary2.getId(), taskSummary3.getId(), + taskSummary4.getId()); + } + } + + private Runnable getRunnableTest(List listedTasks, List accessIds) { + return () -> { + Subject subject = new Subject(); + subject.getPrincipals().add(new UserPrincipal(accessIds.remove(0))); + + Consumer consumer = + CheckedConsumer.wrap( + taskService -> { + internalTaskanaEngine.executeInDatabaseConnection(() -> { + List results = taskService + .createTaskQuery() + .workbasketIdIn(wb1.getId()) + .stateIn(TaskState.READY) + .lockResultsEquals(LOCK_RESULTS_EQUALS).list(); + listedTasks.addAll(results); + for (TaskSummary task : results) { + try { + taskService.claim(task.getId()); + } catch (Exception e) { + throw new SystemException(e.getMessage()); + } + } + }); + }); + + + PrivilegedAction action = + () -> { + consumer.accept(taskService); + return null; + }; + Subject.doAs(subject, action); + }; + } + } + @Nested @TestInstance(Lifecycle.PER_CLASS) class PermissionsTest { diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java b/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java index fb72c306b0..397a193033 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/api/TaskQuery.java @@ -1905,4 +1905,14 @@ TaskQuery orderByCustomIntAttribute( * @return the query */ TaskQuery orderByWorkbasketName(SortDirection sortDirection); + + /** + * This method locks the returned rows until the end of the transaction using for update. + * + * @param lockResults determines the number of returned and locked results; + * if zero, no results are locked, but the number of returned results is not + * limited + * @return the query + */ + TaskQuery lockResultsEquals(Integer lockResults); } diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java index 1f27043688..d1e67cdf14 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQueryImpl.java @@ -337,6 +337,7 @@ public class TaskQueryImpl implements TaskQuery { private CallbackState[] callbackStateNotIn; private WildcardSearchField[] wildcardSearchFieldIn; private String wildcardSearchValueLike; + private Integer lockResults; TaskQueryImpl(InternalTaskanaEngine taskanaEngine) { this.taskanaEngine = taskanaEngine; @@ -345,6 +346,7 @@ public class TaskQueryImpl implements TaskQuery { this.orderByInner = new ArrayList<>(); this.filterByAccessIdIn = true; this.withoutAttachment = false; + this.lockResults = 0; this.joinWithUserInfo = taskanaEngine.getEngine().getConfiguration().isAddAdditionalUserInfo(); } @@ -2116,11 +2118,17 @@ public TaskQuery selectAndClaimEquals(boolean selectAndClaim) { return this; } + public TaskQuery lockResultsEquals(Integer lockResults) { + this.lockResults = lockResults; + return this; + } + // optimized query for db2 can't be used for now in case of selectAndClaim because of temporary // tables and the "for update" clause clashing in db2 private String getLinkToMapperScript() { if (DB.DB2 == getDB() && !selectAndClaim + && lockResults == 0 && taskanaEngine.getEngine().getConfiguration().isUseSpecificDb2Taskquery()) { return LINK_TO_MAPPER_DB2; } else if (selectAndClaim && DB.ORACLE == getDB()) { @@ -2810,8 +2818,8 @@ public String toString() { + Arrays.toString(wildcardSearchFieldIn) + ", wildcardSearchValueLike=" + wildcardSearchValueLike - + ", joinWithUserInfo=" - + joinWithUserInfo + + ", lockResults=" + + lockResults + "]"; } } diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java index 867994f952..6b83c06827 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/TaskQuerySqlProvider.java @@ -72,9 +72,18 @@ public static String queryTaskSummaries() { + " " + "FETCH FIRST ROW ONLY FOR UPDATE " + "" - + "WITH RS USE " + + " " + + "FETCH FIRST ${lockResults} ROWS ONLY FOR UPDATE " + + "" + + "SKIP LOCKED " + + "" + + "" + + "SKIP LOCKED DATA " + + "" + + "" + + "WITH RS USE " + "AND KEEP UPDATE LOCKS " - + "WITH UR " + + "WITH UR " + CLOSING_SCRIPT_TAG; } @@ -146,6 +155,10 @@ public static String queryTaskSummariesDb2() { + "" + "FETCH FIRST ROW ONLY FOR UPDATE WITH RS USE AND KEEP UPDATE LOCKS" + "" + + " " + + "FETCH FIRST #{lockResults} ROWS ONLY FOR UPDATE WITH RS USE AND KEEP UPDATE LOCKS " + + "SKIP LOCKED DATA " + + "" + " with UR" + CLOSING_SCRIPT_TAG; }