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;
}