This is an automated email from the ASF dual-hosted git repository.

martinweiler pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-drools.git


The following commit(s) were added to refs/heads/main by this push:
     new 30c6015f64d [incubator-kie-issues#2382] Handle concurrent duplicate 
ProcessDefinition insert (#6850)
30c6015f64d is described below

commit 30c6015f64dc60626f8fe7e5e74c26f2710e7a11
Author: Abhiram Gundala <[email protected]>
AuthorDate: Thu Jul 30 15:11:32 2026 -0400

    [incubator-kie-issues#2382] Handle concurrent duplicate ProcessDefinition 
insert (#6850)
    
    * Handle concurrent duplicate ProcessDefinition insert
    
    * Simplify transaction wrapping
---
 .../QuarkusProcessDefinitionEntityStorage.java     |   9 ++
 ...greSQLConcurrentProcessDefinitionStorageIT.java |  49 ++++++++
 .../QuarkusProcessDefinitionEntityStorage.java     |   9 ++
 .../storage/DataIndexStorageProducer.java          |  11 --
 .../SpringBootProcessDefinitionEntityStorage.java  |  58 +++++++++
 ...greSQLConcurrentProcessDefinitionStorageIT.java |  56 +++++++++
 .../storage/ProcessDefinitionEntityStorage.java    |  30 ++++-
 ...stractConcurrentProcessDefinitionStorageIT.java | 129 +++++++++++++++++++++
 8 files changed, 339 insertions(+), 12 deletions(-)

diff --git 
a/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-jpa-quarkus/src/main/java/org/kie/kogito/index/jpa/quarkus/QuarkusProcessDefinitionEntityStorage.java
 
b/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-jpa-quarkus/src/main/java/org/kie/kogito/index/jpa/quarkus/QuarkusProcessDefinitionEntityStorage.java
index 253db8f34b4..254e740f1de 100644
--- 
a/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-jpa-quarkus/src/main/java/org/kie/kogito/index/jpa/quarkus/QuarkusProcessDefinitionEntityStorage.java
+++ 
b/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-jpa-quarkus/src/main/java/org/kie/kogito/index/jpa/quarkus/QuarkusProcessDefinitionEntityStorage.java
@@ -19,16 +19,19 @@
 package org.kie.kogito.index.jpa.quarkus;
 
 import java.util.Collections;
+import java.util.function.Supplier;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.kie.kogito.index.jpa.storage.JsonPredicateBuilder;
 import org.kie.kogito.index.jpa.storage.ProcessDefinitionEntityStorage;
+import org.kie.kogito.index.model.ProcessDefinition;
 import org.kie.kogito.process.Processes;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
 import jakarta.persistence.EntityManager;
+import jakarta.transaction.Transactional;
 
 @ApplicationScoped
 public class QuarkusProcessDefinitionEntityStorage extends 
ProcessDefinitionEntityStorage {
@@ -39,4 +42,10 @@ public class QuarkusProcessDefinitionEntityStorage extends 
ProcessDefinitionEnti
         super(em, predicateBuilder, dataIsolationEnabled ? processesInstance : 
Collections.emptyList());
     }
 
+    @Override
+    @Transactional(Transactional.TxType.REQUIRES_NEW)
+    protected ProcessDefinition wrapInTransaction(Supplier<ProcessDefinition> 
supplier) {
+        return supplier.get();
+    }
+
 }
diff --git 
a/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-jpa-quarkus/src/test/java/org/kie/kogito/index/jpa/quarkus/storage/PostgreSQLConcurrentProcessDefinitionStorageIT.java
 
b/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-jpa-quarkus/src/test/java/org/kie/kogito/index/jpa/quarkus/storage/PostgreSQLConcurrentProcessDefinitionStorageIT.java
new file mode 100644
index 00000000000..a9fa5f7a939
--- /dev/null
+++ 
b/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-jpa-quarkus/src/test/java/org/kie/kogito/index/jpa/quarkus/storage/PostgreSQLConcurrentProcessDefinitionStorageIT.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.index.jpa.quarkus.storage;
+
+import javax.sql.DataSource;
+
+import org.kie.kogito.index.jpa.quarkus.PostgreSQLQuarkusTestProfile;
+import 
org.kie.kogito.index.jpa.storage.AbstractConcurrentProcessDefinitionStorageIT;
+import org.kie.kogito.index.jpa.storage.ProcessDefinitionEntityStorage;
+import org.kie.kogito.testcontainers.quarkus.PostgreSqlQuarkusTestResource;
+
+import io.quarkus.narayana.jta.QuarkusTransaction;
+import io.quarkus.test.common.QuarkusTestResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.TestProfile;
+
+import jakarta.inject.Inject;
+
+@QuarkusTest
+@QuarkusTestResource(value = PostgreSqlQuarkusTestResource.class, 
restrictToAnnotatedClass = true)
+@TestProfile(PostgreSQLQuarkusTestProfile.class)
+class PostgreSQLConcurrentProcessDefinitionStorageIT extends 
AbstractConcurrentProcessDefinitionStorageIT {
+
+    @Inject
+    public 
PostgreSQLConcurrentProcessDefinitionStorageIT(ProcessDefinitionEntityStorage 
storage, DataSource dataSource) {
+        super(storage, dataSource);
+    }
+
+    @Override
+    protected void executeInTransaction(Runnable operation) {
+        QuarkusTransaction.requiringNew().run(operation);
+    }
+}
diff --git 
a/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/QuarkusProcessDefinitionEntityStorage.java
 
b/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/QuarkusProcessDefinitionEntityStorage.java
index af93150da40..27dc4e8b88a 100644
--- 
a/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/QuarkusProcessDefinitionEntityStorage.java
+++ 
b/kogito-apps-quarkus/data-index-storage-quarkus/data-index-storage-postgresql/src/main/java/org/kie/kogito/index/postgresql/QuarkusProcessDefinitionEntityStorage.java
@@ -19,16 +19,19 @@
 package org.kie.kogito.index.postgresql;
 
 import java.util.Collections;
+import java.util.function.Supplier;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.kie.kogito.index.jpa.storage.JsonPredicateBuilder;
 import org.kie.kogito.index.jpa.storage.ProcessDefinitionEntityStorage;
+import org.kie.kogito.index.model.ProcessDefinition;
 import org.kie.kogito.process.Processes;
 
 import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
 import jakarta.persistence.EntityManager;
+import jakarta.transaction.Transactional;
 
 @ApplicationScoped
 public class QuarkusProcessDefinitionEntityStorage extends 
ProcessDefinitionEntityStorage {
@@ -39,4 +42,10 @@ public class QuarkusProcessDefinitionEntityStorage extends 
ProcessDefinitionEnti
         super(em, predicateBuilder, dataIsolationEnabled ? processesInstance : 
Collections.emptyList());
     }
 
+    @Override
+    @Transactional(Transactional.TxType.REQUIRES_NEW)
+    protected ProcessDefinition wrapInTransaction(Supplier<ProcessDefinition> 
supplier) {
+        return supplier.get();
+    }
+
 }
diff --git 
a/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/main/java/org/kie/kogito/index/jpa/springboot/storage/DataIndexStorageProducer.java
 
b/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/main/java/org/kie/kogito/index/jpa/springboot/storage/DataIndexStorageProducer.java
index 18b51ef5bf3..537ca431ebd 100644
--- 
a/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/main/java/org/kie/kogito/index/jpa/springboot/storage/DataIndexStorageProducer.java
+++ 
b/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/main/java/org/kie/kogito/index/jpa/springboot/storage/DataIndexStorageProducer.java
@@ -23,7 +23,6 @@ import java.util.Collections;
 import java.util.List;
 
 import org.kie.kogito.index.api.DateTimeCoercing;
-import org.kie.kogito.index.jpa.mapper.ProcessDefinitionEntityMapper;
 import org.kie.kogito.index.jpa.mapper.ProcessInstanceEntityMapper;
 import org.kie.kogito.index.jpa.storage.*;
 import org.kie.kogito.index.storage.DataIndexStorageService;
@@ -45,16 +44,6 @@ public class DataIndexStorageProducer {
         return new JobEntityStorage(entityManager, dataIsolationEnabled ? 
processes : Collections.emptyList());
     }
 
-    @Bean
-    public ProcessDefinitionEntityStorage 
processDefinitionEntityStorage(EntityManager entityManager,
-            @Autowired(required = false) List<JsonPredicateBuilder> 
jsonPredicateBuilders,
-            @Autowired(required = false) List<Processes> processes) {
-        return new ProcessDefinitionEntityStorage(entityManager,
-                jsonPredicateBuilders != null ? jsonPredicateBuilders : 
Collections.emptyList(),
-                ProcessDefinitionEntityMapper.INSTANCE,
-                dataIsolationEnabled ? processes : Collections.emptyList());
-    }
-
     @Bean
     public ProcessInstanceEntityStorage 
processInstanceEntityStorage(EntityManager entityManager,
             @Autowired(required = false) List<JsonPredicateBuilder> 
jsonPredicateBuilders,
diff --git 
a/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/main/java/org/kie/kogito/index/jpa/springboot/storage/SpringBootProcessDefinitionEntityStorage.java
 
b/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/main/java/org/kie/kogito/index/jpa/springboot/storage/SpringBootProcessDefinitionEntityStorage.java
new file mode 100644
index 00000000000..75900d77d3f
--- /dev/null
+++ 
b/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/main/java/org/kie/kogito/index/jpa/springboot/storage/SpringBootProcessDefinitionEntityStorage.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.kie.kogito.index.jpa.springboot.storage;
+
+import java.util.Collections;
+import java.util.function.Supplier;
+
+import org.kie.kogito.index.jpa.mapper.ProcessDefinitionEntityMapper;
+import org.kie.kogito.index.jpa.storage.JsonPredicateBuilder;
+import org.kie.kogito.index.jpa.storage.ProcessDefinitionEntityStorage;
+import org.kie.kogito.index.model.ProcessDefinition;
+import org.kie.kogito.process.Processes;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.support.TransactionTemplate;
+
+import jakarta.persistence.EntityManager;
+
+@Component
+public class SpringBootProcessDefinitionEntityStorage extends 
ProcessDefinitionEntityStorage {
+
+    private final TransactionTemplate isolatedTransaction;
+
+    public SpringBootProcessDefinitionEntityStorage(EntityManager 
entityManager, ObjectProvider<JsonPredicateBuilder> jsonPredicateBuilders,
+            ObjectProvider<Processes> processes, PlatformTransactionManager 
transactionManager,
+            @Value("${kogito.persistence.data-isolation.enabled:false}") 
Boolean dataIsolationEnabled) {
+        super(entityManager, jsonPredicateBuilders, 
ProcessDefinitionEntityMapper.INSTANCE,
+                dataIsolationEnabled ? processes : Collections.emptyList());
+        this.isolatedTransaction = new TransactionTemplate(transactionManager);
+        
this.isolatedTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+    }
+
+    @Override
+    protected ProcessDefinition wrapInTransaction(Supplier<ProcessDefinition> 
supplier) {
+        return isolatedTransaction.execute(status -> supplier.get());
+    }
+
+}
diff --git 
a/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/test/java/org/kie/kogito/index/jpa/springboot/storage/PostgreSQLConcurrentProcessDefinitionStorageIT.java
 
b/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/test/java/org/kie/kogito/index/jpa/springboot/storage/PostgreSQLConcurrentProcessDefinitionStorageIT.java
new file mode 100644
index 00000000000..14690c6956d
--- /dev/null
+++ 
b/kogito-apps-springboot/data-index-springboot/data-index-storage-jpa-springboot/src/test/java/org/kie/kogito/index/jpa/springboot/storage/PostgreSQLConcurrentProcessDefinitionStorageIT.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.index.jpa.springboot.storage;
+
+import javax.sql.DataSource;
+
+import org.kie.kogito.index.jpa.springboot.KogitoSpringBootApplication;
+import 
org.kie.kogito.index.jpa.storage.AbstractConcurrentProcessDefinitionStorageIT;
+import org.kie.kogito.index.jpa.storage.ProcessDefinitionEntityStorage;
+import 
org.kie.kogito.testcontainers.springboot.PostgreSqlSpringBootTestResource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.support.TransactionTemplate;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 
classes = KogitoSpringBootApplication.class)
+@ContextConfiguration(initializers = PostgreSqlSpringBootTestResource.class)
+@ActiveProfiles("postgresql")
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
+class PostgreSQLConcurrentProcessDefinitionStorageIT extends 
AbstractConcurrentProcessDefinitionStorageIT {
+
+    private final TransactionTemplate transactionTemplate;
+
+    @Autowired
+    public 
PostgreSQLConcurrentProcessDefinitionStorageIT(ProcessDefinitionEntityStorage 
storage, DataSource dataSource,
+            PlatformTransactionManager transactionManager) {
+        super(storage, dataSource);
+        this.transactionTemplate = new TransactionTemplate(transactionManager);
+        
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
+    }
+
+    @Override
+    protected void executeInTransaction(Runnable operation) {
+        transactionTemplate.executeWithoutResult(status -> operation.run());
+    }
+}
diff --git 
a/kogito-data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessDefinitionEntityStorage.java
 
b/kogito-data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessDefinitionEntityStorage.java
index 68e5de90c7a..bb8a2d5e952 100644
--- 
a/kogito-data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessDefinitionEntityStorage.java
+++ 
b/kogito-data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/ProcessDefinitionEntityStorage.java
@@ -19,18 +19,26 @@
 package org.kie.kogito.index.jpa.storage;
 
 import java.util.Optional;
+import java.util.function.Supplier;
 
+import org.hibernate.exception.ConstraintViolationException;
 import org.kie.kogito.index.jpa.mapper.ProcessDefinitionEntityMapper;
 import org.kie.kogito.index.jpa.model.ProcessDefinitionEntity;
 import org.kie.kogito.index.model.ProcessDefinition;
 import org.kie.kogito.index.model.ProcessDefinitionKey;
 import org.kie.kogito.process.Processes;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Throwables;
 
 import jakarta.persistence.EntityManager;
 
 import static org.kie.kogito.index.DependencyInjectionUtils.getInstance;
 
-public class ProcessDefinitionEntityStorage extends 
AbstractStorage<ProcessDefinitionKey, ProcessDefinitionEntity, 
ProcessDefinition> {
+public abstract class ProcessDefinitionEntityStorage extends 
AbstractStorage<ProcessDefinitionKey, ProcessDefinitionEntity, 
ProcessDefinition> {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(ProcessDefinitionEntityStorage.class);
 
     protected ProcessDefinitionEntityStorage() {
     }
@@ -44,4 +52,24 @@ public class ProcessDefinitionEntityStorage extends 
AbstractStorage<ProcessDefin
                 e.getVersion()), 
Optional.ofNullable(getInstance(predicateBuilder)), 
Optional.ofNullable(getInstance(processes)));
     }
 
+    @Override
+    public ProcessDefinition put(ProcessDefinitionKey key, ProcessDefinition 
value) {
+        try {
+            return wrapInTransaction(() -> {
+                super.put(key, value);
+                em.flush();
+                return value;
+            });
+        } catch (RuntimeException e) {
+            if 
(Throwables.getCausalChain(e).stream().noneMatch(ConstraintViolationException.class::isInstance))
 {
+                throw e;
+            }
+            LOGGER.info("ProcessDefinition with id '{}' and version '{}' is 
already present, skipping insert.", key.getId(), key.getVersion());
+            LOGGER.debug("Duplicate ProcessDefinition insert suppressed", e);
+            return wrapInTransaction(() -> get(key));
+        }
+    }
+
+    protected abstract ProcessDefinition 
wrapInTransaction(Supplier<ProcessDefinition> supplier);
+
 }
diff --git 
a/kogito-data-index/data-index-storage/data-index-storage-jpa-common/src/test/java/org/kie/kogito/index/jpa/storage/AbstractConcurrentProcessDefinitionStorageIT.java
 
b/kogito-data-index/data-index-storage/data-index-storage-jpa-common/src/test/java/org/kie/kogito/index/jpa/storage/AbstractConcurrentProcessDefinitionStorageIT.java
new file mode 100644
index 00000000000..70b82a53a80
--- /dev/null
+++ 
b/kogito-data-index/data-index-storage/data-index-storage-jpa-common/src/test/java/org/kie/kogito/index/jpa/storage/AbstractConcurrentProcessDefinitionStorageIT.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.kie.kogito.index.jpa.storage;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.sql.DataSource;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.jupiter.api.Test;
+import org.kie.kogito.index.model.ProcessDefinition;
+import org.kie.kogito.index.model.ProcessDefinitionKey;
+import org.kie.kogito.index.test.TestUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+/**
+ * A competing replica is simulated with a plain JDBC connection that inserts 
the same definition and commits
+ * only once the insert of the storage is queued behind it on the primary key 
index, which is what PostgreSQL
+ * does and what makes that insert fail with a duplicate key.
+ */
+public abstract class AbstractConcurrentProcessDefinitionStorageIT {
+
+    private static final String INSERT_DEFINITION = "insert into definitions 
(id, version, name, type) values (?, ?, ?, ?)";
+    private static final String COUNT_PENDING_LOCKS = "select count(*) from 
pg_locks where not granted and pid <> pg_backend_pid()";
+    private static final long LOCK_POLL_TIMEOUT_MILLIS = 15_000;
+    private static final long LOCK_POLL_INTERVAL_MILLIS = 100;
+
+    private final ProcessDefinitionEntityStorage storage;
+    private final DataSource dataSource;
+
+    protected 
AbstractConcurrentProcessDefinitionStorageIT(ProcessDefinitionEntityStorage 
storage, DataSource dataSource) {
+        this.storage = storage;
+        this.dataSource = dataSource;
+    }
+
+    protected abstract void executeInTransaction(Runnable operation);
+
+    @Test
+    void testConcurrentRegistrationOfTheSameProcessDefinition() throws 
Exception {
+        String processId = RandomStringUtils.randomAlphabetic(10);
+        String version = "2.0";
+        ProcessDefinitionKey key = new ProcessDefinitionKey(processId, 
version);
+        ProcessDefinition definition = 
TestUtils.createProcessDefinition(processId, version, Set.of("admin", 
"kogito"));
+
+        AtomicReference<ProcessDefinition> indexed = new AtomicReference<>();
+        AtomicBoolean indexingInsertBlocked = new AtomicBoolean();
+        ExecutorService executor = Executors.newSingleThreadExecutor();
+        CountDownLatch indexingStarted = new CountDownLatch(1);
+        try (Connection otherReplica = dataSource.getConnection()) {
+            otherReplica.setAutoCommit(false);
+            insertDefinition(otherReplica, processId, version);
+
+            Future<?> commitOfTheOtherReplica = executor.submit(() -> {
+                try {
+                    indexingStarted.await();
+                    
indexingInsertBlocked.set(waitForTheIndexingInsertToBlock(otherReplica));
+                } finally {
+                    otherReplica.commit();
+                }
+                return null;
+            });
+
+            indexingStarted.countDown();
+            assertThatCode(() -> executeInTransaction(() -> 
indexed.set(storage.put(key, definition)))).doesNotThrowAnyException();
+
+            commitOfTheOtherReplica.get(1, TimeUnit.MINUTES);
+        } finally {
+            executor.shutdownNow();
+        }
+
+        assertThat(indexingInsertBlocked).as("the storage never queued its 
insert behind the other replica, the race was not reproduced").isTrue();
+        
assertThat(indexed.get()).isNotNull().extracting(ProcessDefinition::getId, 
ProcessDefinition::getVersion, ProcessDefinition::getName)
+                .containsExactly(processId, version, processId);
+        assertThat(storage.get(key)).isEqualTo(indexed.get());
+    }
+
+    private void insertDefinition(Connection connection, String processId, 
String version) throws SQLException {
+        try (PreparedStatement statement = 
connection.prepareStatement(INSERT_DEFINITION)) {
+            statement.setString(1, processId);
+            statement.setString(2, version);
+            statement.setString(3, processId);
+            statement.setString(4, "PROCESS");
+            statement.executeUpdate();
+        }
+    }
+
+    private boolean waitForTheIndexingInsertToBlock(Connection connection) 
throws Exception {
+        long deadline = System.currentTimeMillis() + LOCK_POLL_TIMEOUT_MILLIS;
+        while (System.currentTimeMillis() < deadline) {
+            Thread.sleep(LOCK_POLL_INTERVAL_MILLIS);
+            try (PreparedStatement statement = 
connection.prepareStatement(COUNT_PENDING_LOCKS);
+                    ResultSet resultSet = statement.executeQuery()) {
+                if (resultSet.next() && resultSet.getInt(1) > 0) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to