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 b8121212e21..0b314052d9e 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.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 @@ private List callInTransaction(Consumer tran List 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 @@ private BatchResponse executeRequest(BatchRequest request, UriInfo uriInfo) { 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 5d85df640f7..c8716ff835c 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 @@ protected void doCommit(DefaultTransactionStatus status) { 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 00000000000..2dd6373b1fe --- /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 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 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 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 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 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); + } +}