This is an automated email from the ASF dual-hosted git repository.
arnold 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 bc4b3555f6 FINERACT-2181: Batch API - Fix read-only connection handling
bc4b3555f6 is described below
commit bc4b3555f6b0ca6a13dd016fdee5620dfdd82a10
Author: Adam Saghy <[email protected]>
AuthorDate: Fri Jul 11 19:12:56 2025 +0200
FINERACT-2181: Batch API - Fix read-only connection handling
---
.../batch/service/BatchApiServiceImpl.java | 22 +++++++---
.../batch/service/BatchApiServiceImplTest.java | 51 ++++++++++++++++++++--
2 files changed, 63 insertions(+), 10 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 fbd9b79250..fc41c0f60b 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
@@ -58,6 +58,8 @@ 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.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.lang.NonNull;
@@ -74,9 +76,9 @@ import
org.springframework.transaction.support.TransactionTemplate;
* CommandStrategy from CommandStrategyProvider.
*
* @author Rishabh Shukla
- * @see org.apache.fineract.batch.domain.BatchRequest
- * @see org.apache.fineract.batch.domain.BatchResponse
- * @see org.apache.fineract.batch.command.CommandStrategyProvider
+ * @see BatchRequest
+ * @see BatchResponse
+ * @see CommandStrategyProvider
*/
@Service
@RequiredArgsConstructor
@@ -85,7 +87,6 @@ public class BatchApiServiceImpl implements BatchApiService {
private final CommandStrategyProvider strategyProvider;
private final ResolutionHelper resolutionHelper;
- private final PlatformTransactionManager transactionManager;
private final ErrorHandler errorHandler;
private final List<BatchFilter> batchFilters;
@@ -94,6 +95,7 @@ public class BatchApiServiceImpl implements BatchApiService {
private final RetryConfigurationAssembler retryConfigurationAssembler;
+ private PlatformTransactionManager transactionManager;
private EntityManager entityManager;
/**
@@ -152,6 +154,9 @@ public class BatchApiServiceImpl implements BatchApiService
{
try {
TransactionTemplate transactionTemplate = new
TransactionTemplate(transactionManager);
transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
+ if (transactionManager instanceof
ExtendedJpaTransactionManager extendedJpaTransactionManager) {
+
transactionTemplate.setReadOnly(extendedJpaTransactionManager.isReadOnlyConnection());
+ }
transactionConfigurator.accept(transactionTemplate);
return transactionTemplate.execute(status -> {
BatchRequestContextHolder.setEnclosingTransaction(status);
@@ -175,8 +180,8 @@ public class BatchApiServiceImpl implements BatchApiService
{
}
/**
- * Returns the response list by getting a proper {@link
org.apache.fineract.batch.command.CommandStrategy}.
- * execute() method of acquired commandStrategy is then provided with the
separate Request.
+ * Returns the response list by getting a proper {@link CommandStrategy}.
execute() method of acquired
+ * commandStrategy is then provided with the separate Request.
*
* @param requestList
* @param uriInfo
@@ -395,4 +400,9 @@ public class BatchApiServiceImpl implements BatchApiService
{
public void setEntityManager(EntityManager entityManager) {
this.entityManager = entityManager;
}
+
+ @Autowired
+ public void setTransactionManager(PlatformTransactionManager
transactionManager) {
+ this.transactionManager = transactionManager;
+ }
}
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
index 573aed1a91..ccdcb66284 100644
---
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
@@ -19,11 +19,14 @@
package org.apache.fineract.batch.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -51,6 +54,8 @@ 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.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
@@ -86,8 +91,8 @@ class BatchApiServiceImplTest {
@InjectMocks
private RetryConfigurationAssembler retryConfigurationAssembler;
- private final ResolutionHelper resolutionHelper = Mockito.spy(new
ResolutionHelper(new FromJsonHelper()));
- private final List<BatchRequestPreprocessor> batchPreprocessors =
Mockito.spy(List.of());
+ private final ResolutionHelper resolutionHelper = spy(new
ResolutionHelper(new FromJsonHelper()));
+ private final List<BatchRequestPreprocessor> batchPreprocessors =
spy(List.of());
@InjectMocks
private BatchApiServiceImpl batchApiService;
@@ -96,8 +101,9 @@ class BatchApiServiceImplTest {
@BeforeEach
void setUp() {
- batchApiService = new BatchApiServiceImpl(strategyProvider,
resolutionHelper, transactionManager, errorHandler, List.of(),
- batchPreprocessors, retryConfigurationAssembler);
+ batchApiService = new BatchApiServiceImpl(strategyProvider,
resolutionHelper, errorHandler, List.of(), batchPreprocessors,
+ retryConfigurationAssembler);
+ batchApiService.setTransactionManager(transactionManager);
batchApiService.setEntityManager(entityManager);
request = new BatchRequest();
request.setRequestId(1L);
@@ -227,6 +233,43 @@ class BatchApiServiceImplTest {
Mockito.verifyNoInteractions(entityManager);
}
+ @ParameterizedTest
+ @ValueSource(booleans = { true, false })
+ void testCallInTransactionReadOnlyFlag(boolean isReadOnly) {
+ // Given
+ ExtendedJpaTransactionManager extendedJpaTransactionManager =
mock(ExtendedJpaTransactionManager.class);
+
+ // Create a transaction status with the correct read-only flag
+ DefaultTransactionStatus transactionStatus = new
DefaultTransactionStatus("txn_name", null, true, true, false, isReadOnly, false,
+ null);
+
+ // Mock getTransaction to return our status when the read-only flag
matches
+
when(extendedJpaTransactionManager.isReadOnlyConnection()).thenReturn(isReadOnly);
+ when(extendedJpaTransactionManager
+ .getTransaction(argThat(definition -> definition != null &&
definition.isReadOnly() == isReadOnly)))
+ .thenReturn(transactionStatus);
+
+ // Mock other required dependencies
+
when(strategyProvider.getCommandStrategy(any())).thenReturn(commandStrategy);
+ when(commandStrategy.execute(any(), any())).thenReturn(response);
+
+ batchApiService.setTransactionManager(extendedJpaTransactionManager);
+
+ // Set up a request that will trigger the read-only behavior we want
to test
+ BatchRequest testRequest = new BatchRequest();
+ testRequest.setRequestId(1L);
+ testRequest.setMethod(isReadOnly ? "GET" : "POST"); // Use GET for
read-only, POST for read-write
+ testRequest.setRelativeUrl("/test/endpoint");
+
+ // When
+ List<BatchResponse> responses =
batchApiService.handleBatchRequestsWithEnclosingTransaction(List.of(testRequest),
uriInfo);
+
+ // Then
+ assertFalse(responses.isEmpty());
+ verify(extendedJpaTransactionManager)
+ .getTransaction(argThat(definition -> definition != null &&
definition.isReadOnly() == isReadOnly));
+ }
+
private static final class RetryException extends RuntimeException {}
}