FINERACT-2304: Create Configuration assembler and move resilience4j props into fineract namespace implement proper retry for SynchronousCommandProcessingService and BatchApiServiceImpl

This commit is contained in:
Soma Sörös
2025-06-06 14:03:11 +02:00
committed by Adam Saghy
parent a687227f33
commit caf42f733c
11 changed files with 450 additions and 101 deletions
@@ -441,11 +441,11 @@ spring.batch.initialize-schema=NEVER
# Disabling Spring Batch jobs on startup
spring.batch.job.enabled=false
resilience4j.retry.instances.executeCommand.max-attempts=${FINERACT_COMMAND_PROCESSING_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.executeCommand.wait-duration=${FINERACT_COMMAND_PROCESSING_RETRY_WAIT_DURATION:1s}
resilience4j.retry.instances.executeCommand.enable-exponential-backoff=${FINERACT_COMMAND_PROCESSING_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
resilience4j.retry.instances.executeCommand.exponential-backoff-multiplier=${FINERACT_COMMAND_PROCESSING_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
resilience4j.retry.instances.executeCommand.retryExceptions=${FINERACT_COMMAND_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException,org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException}
fineract.retry.instances.executeCommand.max-attempts=${FINERACT_COMMAND_PROCESSING_RETRY_MAX_ATTEMPTS:3}
fineract.retry.instances.executeCommand.wait-duration=${FINERACT_COMMAND_PROCESSING_RETRY_WAIT_DURATION:1s}
fineract.retry.instances.executeCommand.enable-exponential-backoff=${FINERACT_COMMAND_PROCESSING_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
fineract.retry.instances.executeCommand.exponential-backoff-multiplier=${FINERACT_COMMAND_PROCESSING_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
fineract.retry.instances.executeCommand.retryExceptions=${FINERACT_COMMAND_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.ConcurrencyFailureException,org.eclipse.persistence.exceptions.OptimisticLockException,jakarta.persistence.OptimisticLockException,org.springframework.orm.jpa.JpaOptimisticLockingFailureException,org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException}
resilience4j.retry.instances.processJobDetailForExecution.max-attempts=${FINERACT_PROCESS_JOB_DETAIL_RETRY_MAX_ATTEMPTS:3}
resilience4j.retry.instances.processJobDetailForExecution.wait-duration=${FINERACT_PROCESS_JOB_DETAIL_RETRY_WAIT_DURATION:1s}
@@ -23,10 +23,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import io.cucumber.java8.En;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryRegistry;
import io.github.resilience4j.retry.event.RetryEvent;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.fineract.commands.configuration.RetryConfigurationAssembler;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -50,6 +52,9 @@ public class CommandServiceStepDefinitions implements En {
@Autowired
private RetryRegistry retryRegistry;
@Autowired
private RetryConfigurationAssembler retryConfigurationAssembler;
private PortfolioCommandSourceWritePlatformService commandSourceWritePlatformService;
private DummyCommand command;
@@ -67,8 +72,9 @@ public class CommandServiceStepDefinitions implements En {
Mockito.when(contextHolder.getAttribute(any(), any())).thenThrow(new CannotAcquireLockException("BLOW IT UP!!!"))
.thenThrow(new ObjectOptimisticLockingFailureException("Dummy", new RuntimeException("BLOW IT UP!!!")))
.thenThrow(new RollbackTransactionNotApprovedException(1L, null));
this.retryRegistry.retry("executeCommand").getEventPublisher().onRetry(event -> {
Retry retry1 = retryConfigurationAssembler.getRetryConfigurationForExecuteCommand();
assertNotNull(retry1);
retry1.getEventPublisher().onRetry(event -> {
log.warn("... retry event: {}", event);
counter.incrementAndGet();
@@ -92,7 +98,7 @@ public class CommandServiceStepDefinitions implements En {
assertEquals(2, retryEvent.getNumberOfRetryAttempts());
});
Then("/^The command processing service execute function should be called 3 times$/", () -> {
Then("/^The command processing service execute function should be called 2 times$/", () -> {
assertEquals(2, counter.get());
});
}
@@ -20,12 +20,21 @@ package org.apache.fineract.commands.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Map;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.commands.configuration.RetryConfigurationAssembler;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandWrapper;
@@ -39,6 +48,7 @@ import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidati
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.useradministration.domain.AppUser;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
@@ -70,6 +80,9 @@ public class SynchronousCommandProcessingServiceTest {
@Mock
private CommandSourceService commandSourceService;
@Mock
private RetryConfigurationAssembler retryConfigurationAssembler;
@Spy
private FineractRequestContextHolder fineractRequestContextHolder;
@@ -83,6 +96,71 @@ public class SynchronousCommandProcessingServiceTest {
public void setup() {
MockitoAnnotations.openMocks(this);
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
when(retryConfigurationAssembler.getRetryConfigurationForExecuteCommand())
.thenReturn(Retry.of("IDENTIFIER", RetryConfig.custom().maxAttempts(3).failAfterMaxAttempts(true)
.waitDuration(Duration.ofMillis(1)).retryOnException(e -> e instanceof RetryException).build()));
ErrorInfo errorInfo = mock(ErrorInfo.class);
when(errorInfo.getMessage()).thenReturn("Failed");
when(errorInfo.getStatusCode()).thenReturn(500);
when(commandSourceService.generateErrorInfo(any())).thenReturn(errorInfo);
}
@AfterEach
public void teardown() {
reset(context);
reset(applicationContext);
reset(toApiJsonSerializer);
reset(toApiResultJsonSerializer);
reset(configurationDomainService);
reset(commandHandlerProvider);
reset(idempotencyKeyResolver);
reset(commandSourceService);
reset(retryConfigurationAssembler);
}
@Test
public void testExecuteCommandSuccessAfter2Fails() {
CommandWrapper commandWrapper = Mockito.mock(CommandWrapper.class);
when(commandWrapper.isDatatableResource()).thenReturn(false);
when(commandWrapper.isNoteResource()).thenReturn(false);
when(commandWrapper.isSurveyResource()).thenReturn(false);
when(commandWrapper.isLoanDisburseDetailResource()).thenReturn(false);
long commandId = 1L;
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
when(jsonCommand.commandId()).thenReturn(commandId);
NewCommandSourceHandler commandHandler = Mockito.mock(NewCommandSourceHandler.class);
CommandProcessingResult commandProcessingResult = Mockito.mock(CommandProcessingResult.class);
when(commandProcessingResult.isRollbackTransaction()).thenReturn(false);
when(commandHandler.processCommand(jsonCommand)).thenReturn(commandProcessingResult);
when(commandHandlerProvider.getHandler(Mockito.any(), Mockito.any())).thenReturn(commandHandler);
when(configurationDomainService.isMakerCheckerEnabledForTask(Mockito.any())).thenReturn(false);
String idk = "idk";
when(idempotencyKeyResolver.resolve(commandWrapper)).thenReturn(idk);
CommandSource commandSource = Mockito.mock(CommandSource.class);
when(commandSource.getId()).thenReturn(commandId);
when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(null);
when(commandSourceService.getCommandSource(commandId)).thenReturn(commandSource);
AppUser appUser = Mockito.mock(AppUser.class);
when(commandSourceService.saveInitialNewTransaction(commandWrapper, jsonCommand, appUser, idk)).thenReturn(commandSource);
when(commandSourceService.saveResultSameTransaction(commandSource)).thenReturn(commandSource);
when(commandSource.getStatus()).thenReturn(CommandProcessingResultType.PROCESSED.getValue());
when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
when(commandSourceService.processCommand(commandHandler, jsonCommand, commandSource, appUser, false))
.thenThrow(new RetryException()).thenThrow(new RetryException()).thenReturn(commandProcessingResult);
CommandProcessingResult actualCommandProcessingResult = underTest.executeCommand(commandWrapper, jsonCommand, false);
assertEquals(CommandProcessingResultType.PROCESSED.getValue(), commandSource.getStatus());
assertEquals(commandProcessingResult, actualCommandProcessingResult);
// verify 2x throw before success
verify(commandSourceService, times(2)).generateErrorInfo(any());
verify(commandSourceService).saveResultSameTransaction(commandSource);
}
@Test
@@ -186,4 +264,7 @@ public class SynchronousCommandProcessingServiceTest {
underTest.publishHookEvent(entityName, actionName, command, Object.class);
});
}
private static final class RetryException extends RuntimeException {}
}
@@ -259,11 +259,11 @@ spring.batch.initialize-schema=NEVER
# Disabling Spring Batch jobs on startup
spring.batch.job.enabled=false
resilience4j.retry.instances.executeCommand.max-attempts=3
resilience4j.retry.instances.executeCommand.wait-duration=1s
resilience4j.retry.instances.executeCommand.enable-exponential-backoff=true
resilience4j.retry.instances.executeCommand.exponential-backoff-multiplier=2
resilience4j.retry.instances.executeCommand.retryExceptions=org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException
fineract.retry.instances.executeCommand.max-attempts=3
fineract.retry.instances.executeCommand.wait-duration=1s
fineract.retry.instances.executeCommand.enable-exponential-backoff=true
fineract.retry.instances.executeCommand.exponential-backoff-multiplier=2
fineract.retry.instances.executeCommand.retryExceptions=org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException
resilience4j.retry.instances.processJobDetailForExecution.max-attempts=3
resilience4j.retry.instances.processJobDetailForExecution.wait-duration=1s
@@ -19,9 +19,10 @@
Feature: Commands Service
@soma
Scenario: Verify that command source write service are working with fallback function
Given A command source write service
When The user executes the command via a command write service with exceptions
Then The command processing service should fallback as expected
Then The command processing service execute function should be called 3 times
Then The command processing service execute function should be called 2 times