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

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new d03973165 FINERACT-2081: Support read-only transactions via Batch API
d03973165 is described below

commit d03973165dbd52b202775a277d34eeadc02830f9
Author: Adam Saghy <[email protected]>
AuthorDate: Mon Dec 16 18:52:15 2024 +0100

    FINERACT-2081: Support read-only transactions via Batch API
---
 .../batch/service/BatchApiServiceImpl.java         |   7 +-
 ...abaseSelectingPersistenceUnitPostProcessor.java |   0
 .../persistence/ExtendedJpaTransactionManager.java |   2 +-
 .../persistence/TransactionLifecycleCallback.java  |   0
 .../batch/service/BatchApiServiceImplTest.java     | 118 +++++++++++++++++++++
 5 files changed, 125 insertions(+), 2 deletions(-)

diff --git 
a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java
 
b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java
index b8121212e..0b314052d 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/batch/service/BatchApiServiceImpl.java
@@ -56,6 +56,7 @@ import 
org.apache.fineract.infrastructure.core.exception.ErrorHandler;
 import org.apache.fineract.infrastructure.core.filters.BatchCallHandler;
 import org.apache.fineract.infrastructure.core.filters.BatchFilter;
 import 
org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
+import 
org.apache.fineract.infrastructure.core.persistence.ExtendedJpaTransactionManager;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.dao.ConcurrencyFailureException;
 import org.springframework.dao.NonTransientDataAccessException;
@@ -141,6 +142,9 @@ public class BatchApiServiceImpl implements BatchApiService 
{
         List<BatchResponse> responseList = new ArrayList<>();
         try {
             TransactionTemplate transactionTemplate = new 
TransactionTemplate(transactionManager);
+            if (transactionManager instanceof ExtendedJpaTransactionManager 
extendedJpaTransactionManager) {
+                
transactionTemplate.setReadOnly(extendedJpaTransactionManager.isReadOnlyConnection());
+            }
             transactionConfigurator.accept(transactionTemplate);
             return transactionTemplate.execute(status -> {
                 BatchRequestContextHolder.setEnclosingTransaction(status);
@@ -240,7 +244,8 @@ public class BatchApiServiceImpl implements BatchApiService 
{
             BatchRequestContextHolder.setRequestAttributes(new 
HashMap<>(Optional.ofNullable(request.getHeaders())
                     .map(list -> 
list.stream().collect(Collectors.toMap(Header::getName, Header::getValue)))
                     .orElse(Collections.emptyMap())));
-            if (BatchRequestContextHolder.isEnclosingTransaction()) {
+            if (BatchRequestContextHolder.isEnclosingTransaction()
+                    && 
BatchRequestContextHolder.getEnclosingTransaction().stream().anyMatch(ts -> 
!ts.isReadOnly())) {
                 entityManager.flush();
             }
             BatchCallHandler callHandler = new 
BatchCallHandler(this.batchFilters, commandStrategy::execute);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/DatabaseSelectingPersistenceUnitPostProcessor.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/DatabaseSelectingPersistenceUnitPostProcessor.java
similarity index 100%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/DatabaseSelectingPersistenceUnitPostProcessor.java
rename to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/DatabaseSelectingPersistenceUnitPostProcessor.java
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
similarity index 98%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
rename to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
index 5d85df640..c8716ff83 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/ExtendedJpaTransactionManager.java
@@ -64,7 +64,7 @@ public class ExtendedJpaTransactionManager extends 
JpaTransactionManager {
         invokeLifecycleCallbacks(TransactionLifecycleCallback::afterCommit);
     }
 
-    private boolean isReadOnlyConnection() {
+    public boolean isReadOnlyConnection() {
         try (Connection connection = getDataSource().getConnection()) {
             return connection.isReadOnly();
         } catch (SQLException e) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java
similarity index 100%
rename from 
fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java
rename to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/persistence/TransactionLifecycleCallback.java
diff --git 
a/fineract-core/src/test/java/org/apache/fineract/batch/service/BatchApiServiceImplTest.java
 
b/fineract-core/src/test/java/org/apache/fineract/batch/service/BatchApiServiceImplTest.java
new file mode 100644
index 000000000..2dd6373b1
--- /dev/null
+++ 
b/fineract-core/src/test/java/org/apache/fineract/batch/service/BatchApiServiceImplTest.java
@@ -0,0 +1,118 @@
+/**
+ * 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.apache.fineract.batch.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
+
+import jakarta.persistence.EntityManager;
+import jakarta.ws.rs.core.UriInfo;
+import java.util.List;
+import org.apache.fineract.batch.command.CommandStrategy;
+import org.apache.fineract.batch.command.CommandStrategyProvider;
+import org.apache.fineract.batch.domain.BatchRequest;
+import org.apache.fineract.batch.domain.BatchResponse;
+import 
org.apache.fineract.infrastructure.core.filters.BatchRequestPreprocessor;
+import 
org.apache.fineract.infrastructure.core.persistence.ExtendedJpaTransactionManager;
+import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.transaction.support.DefaultTransactionStatus;
+
+@ExtendWith(MockitoExtension.class)
+class BatchApiServiceImplTest {
+
+    @Mock
+    private CommandStrategyProvider strategyProvider;
+    @Mock
+    private ExtendedJpaTransactionManager transactionManager;
+    @Mock
+    private EntityManager entityManager;
+    @Mock
+    private CommandStrategy commandStrategy;
+    @Mock
+    private UriInfo uriInfo;
+    private final ResolutionHelper resolutionHelper = Mockito.spy(new 
ResolutionHelper(new FromJsonHelper()));
+    private final List<BatchRequestPreprocessor> batchPreprocessors = 
Mockito.spy(List.of());
+    @InjectMocks
+    private BatchApiServiceImpl batchApiService;
+    private BatchRequest request;
+    private BatchResponse response;
+
+    @BeforeEach
+    void setUp() {
+        request = new BatchRequest();
+        request.setRequestId(1L);
+        request.setMethod("POST");
+        request.setRelativeUrl("/random_api");
+        response = new BatchResponse();
+        response.setRequestId(1L);
+        response.setStatusCode(200);
+        response.setBody("Success");
+    }
+
+    @AfterEach
+    void tearDown() {
+        Mockito.reset(resolutionHelper);
+        Mockito.reset(batchPreprocessors);
+        Mockito.reset(entityManager);
+        Mockito.reset(commandStrategy);
+        Mockito.reset(strategyProvider);
+        Mockito.reset(transactionManager);
+    }
+
+    @Test
+    void testHandleBatchRequestsWithEnclosingTransaction() {
+        List<BatchRequest> requestList = List.of(request);
+        
when(strategyProvider.getCommandStrategy(any())).thenReturn(commandStrategy);
+        when(commandStrategy.execute(any(), any())).thenReturn(response);
+        // Regular transaction
+        when(transactionManager.getTransaction(any()))
+                .thenReturn(new DefaultTransactionStatus("txn_name", null, 
true, true, false, false, false, null));
+        List<BatchResponse> result = 
batchApiService.handleBatchRequestsWithEnclosingTransaction(requestList, 
uriInfo);
+        assertEquals(1, result.size());
+        assertEquals(200, result.get(0).getStatusCode());
+        assertTrue(result.get(0).getBody().contains("Success"));
+        Mockito.verify(entityManager, times(1)).flush();
+    }
+
+    @Test
+    void testHandleBatchRequestsWithEnclosingTransactionReadOnly() {
+        List<BatchRequest> requestList = List.of(request);
+        
when(strategyProvider.getCommandStrategy(any())).thenReturn(commandStrategy);
+        when(commandStrategy.execute(any(), any())).thenReturn(response);
+        // Read-only transaction
+        when(transactionManager.getTransaction(any()))
+                .thenReturn(new DefaultTransactionStatus("txn_name", null, 
true, true, false, true, false, null));
+        List<BatchResponse> result = 
batchApiService.handleBatchRequestsWithEnclosingTransaction(requestList, 
uriInfo);
+        assertEquals(1, result.size());
+        assertEquals(200, result.get(0).getStatusCode());
+        assertTrue(result.get(0).getBody().contains("Success"));
+        Mockito.verifyNoInteractions(entityManager);
+    }
+}

Reply via email to