FINERACT-1795: Improve resilience of command processing service
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
aa7747c5ce
commit
744abd1364
@@ -187,5 +187,7 @@ dependencyManagement {
|
|||||||
dependency 'org.mapstruct:mapstruct-processor:1.5.3.Final'
|
dependency 'org.mapstruct:mapstruct-processor:1.5.3.Final'
|
||||||
|
|
||||||
dependency "org.apache.avro:avro:1.11.1"
|
dependency "org.apache.avro:avro:1.11.1"
|
||||||
|
|
||||||
|
dependency "io.github.resilience4j:resilience4j-spring-boot2:1.7.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
= Command
|
||||||
|
|
||||||
|
== `CommandProcessingService`
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
.Retry-able service function `executeCommand`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java[lines=73..151]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Fallback function `fallbackExecuteCommand`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java[lines=166..174]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Retry configuration for `executeCommand`
|
||||||
|
[source,properties]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/resources/application.properties[lines=169..174]
|
||||||
|
----
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
= Resilience
|
||||||
|
|
||||||
|
include::intro.adoc[leveloffset=+1]
|
||||||
|
|
||||||
|
include::command.adoc[leveloffset=+1]
|
||||||
|
|
||||||
|
include::job.adoc[leveloffset=+1]
|
||||||
|
|
||||||
|
include::loan.adoc[leveloffset=+1]
|
||||||
|
|
||||||
|
include::saving.adoc[leveloffset=+1]
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
= Introduction Resilience
|
||||||
|
|
||||||
|
Fineract had handcrafted retry loops in place for the longest time. A typical retry code would have looked like this:
|
||||||
|
|
||||||
|
.Legacy retry code
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("AvoidHidingCauseException")
|
||||||
|
@SuppressFBWarnings(value = {
|
||||||
|
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
||||||
|
public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) {
|
||||||
|
|
||||||
|
boolean isApprovedByChecker = false;
|
||||||
|
// check if is update of own account details
|
||||||
|
if (wrapper.isUpdateOfOwnUserDetails(this.context.authenticatedUser(wrapper).getId())) {
|
||||||
|
// then allow this operation to proceed.
|
||||||
|
// maker checker doesnt mean anything here.
|
||||||
|
isApprovedByChecker = true; // set to true in case permissions have
|
||||||
|
// been maker-checker enabled by
|
||||||
|
// accident.
|
||||||
|
} else {
|
||||||
|
// if not user changing their own details - check user has
|
||||||
|
// permission to perform specific task.
|
||||||
|
this.context.authenticatedUser(wrapper).validateHasPermissionTo(wrapper.getTaskPermissionName());
|
||||||
|
}
|
||||||
|
validateIsUpdateAllowed();
|
||||||
|
|
||||||
|
final String json = wrapper.getJson();
|
||||||
|
CommandProcessingResult result = null;
|
||||||
|
JsonCommand command;
|
||||||
|
int numberOfRetries = 0; // <1>
|
||||||
|
int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
||||||
|
int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
|
||||||
|
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(json);
|
||||||
|
command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(),
|
||||||
|
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
|
||||||
|
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
|
||||||
|
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
|
||||||
|
while (numberOfRetries <= maxNumberOfRetries) { // <2>
|
||||||
|
try {
|
||||||
|
result = this.processAndLogCommandService.executeCommand(wrapper, command, isApprovedByChecker);
|
||||||
|
numberOfRetries = maxNumberOfRetries + 1; // <3>
|
||||||
|
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
|
||||||
|
log.debug("The following command {} has been retried {} time(s)", command.json(), numberOfRetries);
|
||||||
|
/***
|
||||||
|
* Fail if the transaction has been retired for maxNumberOfRetries
|
||||||
|
**/
|
||||||
|
if (numberOfRetries >= maxNumberOfRetries) {
|
||||||
|
log.warn("The following command {} has been retried for the max allowed attempts of {} and will be rolled back",
|
||||||
|
command.json(), numberOfRetries);
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
/***
|
||||||
|
* Else sleep for a random time (between 1 to 10 seconds) and continue
|
||||||
|
**/
|
||||||
|
try {
|
||||||
|
int randomNum = RANDOM.nextInt(maxIntervalBetweenRetries + 1);
|
||||||
|
Thread.sleep(1000 + (randomNum * 1000));
|
||||||
|
numberOfRetries = numberOfRetries + 1; // <4>
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
} catch (final RollbackTransactionAsCommandIsNotApprovedByCheckerException e) {
|
||||||
|
numberOfRetries = maxNumberOfRetries + 1; // <3>
|
||||||
|
result = this.processAndLogCommandService.logCommand(e.getCommandSourceResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
----
|
||||||
|
<1> counter
|
||||||
|
<2> `while` loop
|
||||||
|
<3> increment to abort
|
||||||
|
<4> increment
|
||||||
|
|
||||||
|
For better code quality and readability we introduced https://resilience4j.readme.io/docs[Resilience4j]:
|
||||||
|
|
||||||
|
.Annotation based retry
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java[lines=49..76]
|
||||||
|
----
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
= Jobs
|
||||||
|
|
||||||
|
== `SchedularWritePlatformService`
|
||||||
|
|
||||||
|
WARNING: This service has a typo and should be called `SchedulerWritePlatformService`.
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
.Retry-able service function `processJobDetailForExecution`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedularWritePlatformServiceJpaRepositoryImpl.java[lines=135..155]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Fallback function `fallbackProcessJobDetailForExecution`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedularWritePlatformServiceJpaRepositoryImpl.java[lines=156..159]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Retry configuration for `processJobDetailForExecution`
|
||||||
|
[source,properties]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/resources/application.properties[lines=175..179]
|
||||||
|
----
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
= Loan
|
||||||
|
|
||||||
|
== `LoanWritePlatformService`
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
.Retry-able service function `recalculateInterest`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java[lines=3217..3247]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Fallback function `fallbackRecalculateInterest`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java[lines=3255..3266]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Retry configuration for `recalculateInterest`
|
||||||
|
[source,properties]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/resources/application.properties[lines=180..185]
|
||||||
|
----
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
= Savings
|
||||||
|
|
||||||
|
== `SavingsAccountWritePlatformService`
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
.Retry-able service function `postInterest`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java[lines=586..627]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Fallback function `fallbackPostInterest`
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java[lines=1402..1414]
|
||||||
|
----
|
||||||
|
|
||||||
|
.Retry configuration for `postInterest`
|
||||||
|
[source,properties]
|
||||||
|
----
|
||||||
|
include::{rootdir}/fineract-provider/src/main/resources/application.properties[lines=186..191]
|
||||||
|
----
|
||||||
@@ -39,6 +39,8 @@ include::chapters/development/index.adoc[leveloffset=+1]
|
|||||||
|
|
||||||
include::chapters/custom/index.adoc[leveloffset=+1]
|
include::chapters/custom/index.adoc[leveloffset=+1]
|
||||||
|
|
||||||
|
include::chapters/resilience/index.adoc[leveloffset=+1]
|
||||||
|
|
||||||
include::chapters/security/index.adoc[leveloffset=+1]
|
include::chapters/security/index.adoc[leveloffset=+1]
|
||||||
|
|
||||||
include::chapters/testing/index.adoc[leveloffset=+1]
|
include::chapters/testing/index.adoc[leveloffset=+1]
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ dependencies {
|
|||||||
'org.springdoc:springdoc-openapi-common',
|
'org.springdoc:springdoc-openapi-common',
|
||||||
'org.springdoc:springdoc-openapi-security',
|
'org.springdoc:springdoc-openapi-security',
|
||||||
'org.mapstruct:mapstruct',
|
'org.mapstruct:mapstruct',
|
||||||
|
|
||||||
|
'io.github.resilience4j:resilience4j-spring-boot2',
|
||||||
)
|
)
|
||||||
|
|
||||||
implementation ('org.apache.commons:commons-email') {
|
implementation ('org.apache.commons:commons-email') {
|
||||||
|
|||||||
+5
-50
@@ -19,8 +19,6 @@
|
|||||||
package org.apache.fineract.commands.service;
|
package org.apache.fineract.commands.service;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.commands.domain.CommandSource;
|
import org.apache.fineract.commands.domain.CommandSource;
|
||||||
@@ -28,16 +26,12 @@ import org.apache.fineract.commands.domain.CommandSourceRepository;
|
|||||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
import org.apache.fineract.commands.exception.CommandNotAwaitingApprovalException;
|
import org.apache.fineract.commands.exception.CommandNotAwaitingApprovalException;
|
||||||
import org.apache.fineract.commands.exception.CommandNotFoundException;
|
import org.apache.fineract.commands.exception.CommandNotFoundException;
|
||||||
import org.apache.fineract.commands.exception.RollbackTransactionAsCommandIsNotApprovedByCheckerException;
|
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
|
||||||
import org.apache.fineract.infrastructure.jobs.service.SchedulerJobRunnerReadService;
|
import org.apache.fineract.infrastructure.jobs.service.SchedulerJobRunnerReadService;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.springframework.dao.CannotAcquireLockException;
|
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -46,8 +40,6 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class PortfolioCommandSourceWritePlatformServiceImpl implements PortfolioCommandSourceWritePlatformService {
|
public class PortfolioCommandSourceWritePlatformServiceImpl implements PortfolioCommandSourceWritePlatformService {
|
||||||
|
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final CommandSourceRepository commandSourceRepository;
|
private final CommandSourceRepository commandSourceRepository;
|
||||||
private final FromJsonHelper fromApiJsonHelper;
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
@@ -55,12 +47,10 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("AvoidHidingCauseException")
|
|
||||||
@SuppressFBWarnings(value = {
|
|
||||||
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
|
||||||
public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) {
|
public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) {
|
||||||
|
|
||||||
boolean isApprovedByChecker = false;
|
boolean isApprovedByChecker = false;
|
||||||
|
|
||||||
// check if is update of own account details
|
// check if is update of own account details
|
||||||
if (wrapper.isUpdateOfOwnUserDetails(this.context.authenticatedUser(wrapper).getId())) {
|
if (wrapper.isUpdateOfOwnUserDetails(this.context.authenticatedUser(wrapper).getId())) {
|
||||||
// then allow this operation to proceed.
|
// then allow this operation to proceed.
|
||||||
@@ -76,47 +66,13 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
validateIsUpdateAllowed();
|
validateIsUpdateAllowed();
|
||||||
|
|
||||||
final String json = wrapper.getJson();
|
final String json = wrapper.getJson();
|
||||||
CommandProcessingResult result = null;
|
|
||||||
JsonCommand command;
|
|
||||||
int numberOfRetries = 0;
|
|
||||||
int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
|
||||||
int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
|
|
||||||
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(json);
|
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(json);
|
||||||
command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(),
|
JsonCommand command = JsonCommand.from(json, parsedCommand, this.fromApiJsonHelper, wrapper.getEntityName(), wrapper.getEntityId(),
|
||||||
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
|
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
|
||||||
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
|
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
|
||||||
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
|
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
|
||||||
while (numberOfRetries <= maxNumberOfRetries) {
|
|
||||||
try {
|
|
||||||
result = this.processAndLogCommandService.executeCommand(wrapper, command, isApprovedByChecker);
|
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
|
|
||||||
log.debug("The following command {} has been retried {} time(s)", command.json(), numberOfRetries);
|
|
||||||
/***
|
|
||||||
* Fail if the transaction has been retired for maxNumberOfRetries
|
|
||||||
**/
|
|
||||||
if (numberOfRetries >= maxNumberOfRetries) {
|
|
||||||
log.warn("The following command {} has been retried for the max allowed attempts of {} and will be rolled back",
|
|
||||||
command.json(), numberOfRetries);
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
/***
|
|
||||||
* Else sleep for a random time (between 1 to 10 seconds) and continue
|
|
||||||
**/
|
|
||||||
try {
|
|
||||||
int randomNum = RANDOM.nextInt(maxIntervalBetweenRetries + 1);
|
|
||||||
Thread.sleep(1000 + (randomNum * 1000));
|
|
||||||
numberOfRetries = numberOfRetries + 1;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
} catch (final RollbackTransactionAsCommandIsNotApprovedByCheckerException e) {
|
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
result = this.processAndLogCommandService.logCommand(e.getCommandSourceResult());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return this.processAndLogCommandService.executeCommand(wrapper, command, isApprovedByChecker);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -168,9 +124,8 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
|||||||
return commandSourceInput;
|
return commandSourceInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validateIsUpdateAllowed() {
|
private void validateIsUpdateAllowed() {
|
||||||
return this.schedulerJobRunnerReadService.isUpdatesAllowed();
|
this.schedulerJobRunnerReadService.isUpdatesAllowed();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+12
-1
@@ -20,6 +20,7 @@ package org.apache.fineract.commands.service;
|
|||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import io.github.resilience4j.retry.annotation.Retry;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -69,8 +70,9 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
|||||||
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
|
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
|
||||||
private final FineractProperties fineractProperties;
|
private final FineractProperties fineractProperties;
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
|
@Retry(name = "executeCommand", fallbackMethod = "fallbackExecuteCommand")
|
||||||
public CommandProcessingResult executeCommand(final CommandWrapper wrapper, final JsonCommand command,
|
public CommandProcessingResult executeCommand(final CommandWrapper wrapper, final JsonCommand command,
|
||||||
final boolean isApprovedByChecker) {
|
final boolean isApprovedByChecker) {
|
||||||
|
|
||||||
@@ -161,6 +163,15 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
|||||||
.withEntityId(commandSourceResult.getResourceId()).build();
|
.withEntityId(commandSourceResult.getResourceId()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private CommandProcessingResult fallbackExecuteCommand(Exception e) throws Exception {
|
||||||
|
if (e instanceof RollbackTransactionAsCommandIsNotApprovedByCheckerException ex) {
|
||||||
|
return logCommand(ex.getCommandSourceResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
private NewCommandSourceHandler findCommandHandler(final CommandWrapper wrapper) {
|
private NewCommandSourceHandler findCommandHandler(final CommandWrapper wrapper) {
|
||||||
NewCommandSourceHandler handler;
|
NewCommandSourceHandler handler;
|
||||||
|
|
||||||
|
|||||||
+3
-16
@@ -54,8 +54,6 @@ public class FineractPlatformTenantConnection implements Serializable {
|
|||||||
private final int suspectTimeout;
|
private final int suspectTimeout;
|
||||||
private final int timeBetweenEvictionRunsMillis;
|
private final int timeBetweenEvictionRunsMillis;
|
||||||
private final int minEvictableIdleTimeMillis;
|
private final int minEvictableIdleTimeMillis;
|
||||||
private final int maxRetriesOnDeadlock;
|
|
||||||
private final int maxIntervalBetweenRetries;
|
|
||||||
private final boolean testOnBorrow;
|
private final boolean testOnBorrow;
|
||||||
|
|
||||||
public FineractPlatformTenantConnection(final Long connectionId, final String schemaName, String schemaServer,
|
public FineractPlatformTenantConnection(final Long connectionId, final String schemaName, String schemaServer,
|
||||||
@@ -63,10 +61,9 @@ public class FineractPlatformTenantConnection implements Serializable {
|
|||||||
final String schemaPassword, final boolean autoUpdateEnabled, final int initialSize, final long validationInterval,
|
final String schemaPassword, final boolean autoUpdateEnabled, final int initialSize, final long validationInterval,
|
||||||
final boolean removeAbandoned, final int removeAbandonedTimeout, final boolean logAbandoned,
|
final boolean removeAbandoned, final int removeAbandonedTimeout, final boolean logAbandoned,
|
||||||
final int abandonWhenPercentageFull, final int maxActive, final int minIdle, final int maxIdle, final int suspectTimeout,
|
final int abandonWhenPercentageFull, final int maxActive, final int minIdle, final int maxIdle, final int suspectTimeout,
|
||||||
final int timeBetweenEvictionRunsMillis, final int minEvictableIdleTimeMillis, final int maxRetriesOnDeadlock,
|
final int timeBetweenEvictionRunsMillis, final int minEvictableIdleTimeMillis, final boolean tesOnBorrow,
|
||||||
final int maxIntervalBetweenRetries, final boolean tesOnBorrow, final String readOnlySchemaServer,
|
final String readOnlySchemaServer, final String readOnlySchemaServerPort, final String readOnlySchemaName,
|
||||||
final String readOnlySchemaServerPort, final String readOnlySchemaName, final String readOnlySchemaUsername,
|
final String readOnlySchemaUsername, final String readOnlySchemaPassword, final String readOnlySchemaConnectionParameters) {
|
||||||
final String readOnlySchemaPassword, final String readOnlySchemaConnectionParameters) {
|
|
||||||
|
|
||||||
this.connectionId = connectionId;
|
this.connectionId = connectionId;
|
||||||
this.schemaName = schemaName;
|
this.schemaName = schemaName;
|
||||||
@@ -88,8 +85,6 @@ public class FineractPlatformTenantConnection implements Serializable {
|
|||||||
this.suspectTimeout = suspectTimeout;
|
this.suspectTimeout = suspectTimeout;
|
||||||
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
|
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
|
||||||
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
|
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
|
||||||
this.maxRetriesOnDeadlock = maxRetriesOnDeadlock;
|
|
||||||
this.maxIntervalBetweenRetries = maxIntervalBetweenRetries;
|
|
||||||
this.testOnBorrow = tesOnBorrow;
|
this.testOnBorrow = tesOnBorrow;
|
||||||
this.readOnlySchemaServer = readOnlySchemaServer;
|
this.readOnlySchemaServer = readOnlySchemaServer;
|
||||||
this.readOnlySchemaServerPort = readOnlySchemaServerPort;
|
this.readOnlySchemaServerPort = readOnlySchemaServerPort;
|
||||||
@@ -171,14 +166,6 @@ public class FineractPlatformTenantConnection implements Serializable {
|
|||||||
return this.minEvictableIdleTimeMillis;
|
return this.minEvictableIdleTimeMillis;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getMaxRetriesOnDeadlock() {
|
|
||||||
return this.maxRetriesOnDeadlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getMaxIntervalBetweenRetries() {
|
|
||||||
return this.maxIntervalBetweenRetries;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTestOnBorrow() {
|
public boolean isTestOnBorrow() {
|
||||||
return testOnBorrow;
|
return testOnBorrow;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -18,6 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.infrastructure.jobs.service;
|
package org.apache.fineract.infrastructure.jobs.service;
|
||||||
|
|
||||||
|
import io.github.resilience4j.retry.annotation.Retry;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -133,6 +134,7 @@ public class SchedularWritePlatformServiceJpaRepositoryImpl implements Schedular
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
|
@Retry(name = "processJobDetailForExecution", fallbackMethod = "fallbackProcessJobDetailForExecution")
|
||||||
public boolean processJobDetailForExecution(final String jobKey, final String triggerType) {
|
public boolean processJobDetailForExecution(final String jobKey, final String triggerType) {
|
||||||
boolean isStopExecution = false;
|
boolean isStopExecution = false;
|
||||||
final ScheduledJobDetail scheduledJobDetail = this.scheduledJobDetailsRepository.findByJobKeyWithLock(jobKey);
|
final ScheduledJobDetail scheduledJobDetail = this.scheduledJobDetailsRepository.findByJobKeyWithLock(jobKey);
|
||||||
@@ -151,4 +153,9 @@ public class SchedularWritePlatformServiceJpaRepositoryImpl implements Schedular
|
|||||||
return isStopExecution;
|
return isStopExecution;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private boolean fallbackProcessJobDetailForExecution(Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-38
@@ -18,11 +18,10 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.infrastructure.jobs.service;
|
package org.apache.fineract.infrastructure.jobs.service;
|
||||||
|
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||||
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
|
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
||||||
@@ -34,19 +33,15 @@ import org.quartz.JobKey;
|
|||||||
import org.quartz.Trigger;
|
import org.quartz.Trigger;
|
||||||
import org.quartz.Trigger.CompletedExecutionInstruction;
|
import org.quartz.Trigger.CompletedExecutionInstruction;
|
||||||
import org.quartz.TriggerListener;
|
import org.quartz.TriggerListener;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Isolation;
|
import org.springframework.transaction.annotation.Isolation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Component
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Component
|
||||||
public class SchedulerTriggerListener implements TriggerListener {
|
public class SchedulerTriggerListener implements TriggerListener {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SchedulerTriggerListener.class);
|
|
||||||
private static final SecureRandom random = new SecureRandom();
|
|
||||||
|
|
||||||
private final SchedularWritePlatformService schedularService;
|
private final SchedularWritePlatformService schedularService;
|
||||||
private final TenantDetailsService tenantDetailsService;
|
private final TenantDetailsService tenantDetailsService;
|
||||||
|
|
||||||
@@ -59,12 +54,10 @@ public class SchedulerTriggerListener implements TriggerListener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void triggerFired(Trigger trigger, JobExecutionContext context) {
|
public void triggerFired(Trigger trigger, JobExecutionContext context) {
|
||||||
LOG.debug("triggerFired() trigger={}, context={}", trigger, context);
|
log.debug("triggerFired() trigger={}, context={}", trigger, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressFBWarnings(value = {
|
|
||||||
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
|
||||||
@Transactional(isolation = Isolation.READ_COMMITTED)
|
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||||
public boolean vetoJobExecution(final Trigger trigger, final JobExecutionContext context) {
|
public boolean vetoJobExecution(final Trigger trigger, final JobExecutionContext context) {
|
||||||
final String tenantIdentifier = trigger.getJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
|
final String tenantIdentifier = trigger.getJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
|
||||||
@@ -79,43 +72,22 @@ public class SchedulerTriggerListener implements TriggerListener {
|
|||||||
if (context.getMergedJobDataMap().containsKey(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE)) {
|
if (context.getMergedJobDataMap().containsKey(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE)) {
|
||||||
triggerType = context.getMergedJobDataMap().getString(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE);
|
triggerType = context.getMergedJobDataMap().getString(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE);
|
||||||
}
|
}
|
||||||
Integer maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
boolean vetoJob = this.schedularService.processJobDetailForExecution(jobKey, triggerType);
|
||||||
Integer maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
|
|
||||||
Integer numberOfRetries = 0;
|
|
||||||
boolean vetoJob = false;
|
|
||||||
while (numberOfRetries <= maxNumberOfRetries) {
|
|
||||||
try {
|
|
||||||
vetoJob = this.schedularService.processJobDetailForExecution(jobKey, triggerType);
|
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
} catch (Exception exception) { // Adding generic exception as it
|
|
||||||
// depends on JPA provider
|
|
||||||
LOG.warn("vetoJobExecution() not able to acquire the lock to update job running status at retry {} (of {}) for JobKey: {}",
|
|
||||||
numberOfRetries, maxNumberOfRetries, jobKey, exception);
|
|
||||||
try {
|
|
||||||
int randomNum = random.nextInt(maxIntervalBetweenRetries + 1);
|
|
||||||
Thread.sleep(1000 + (randomNum * 1000));
|
|
||||||
numberOfRetries = numberOfRetries + 1;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOG.error("vetoJobExecution() caught an InterruptedException", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (vetoJob) {
|
if (vetoJob) {
|
||||||
LOG.warn(
|
log.warn(
|
||||||
"vetoJobExecution() WILL veto the execution (returning vetoJob == true; the job's execute method will NOT be called); "
|
"vetoJobExecution() WILL veto the execution (returning vetoJob == true; the job's execute method will NOT be called); tenant={}, jobKey={}, triggerType={}, trigger={}, context={}",
|
||||||
+ "maxNumberOfRetries={}, tenant={}, jobKey={}, triggerType={}, trigger={}, context={}",
|
tenantIdentifier, jobKey, triggerType, trigger, context);
|
||||||
maxNumberOfRetries, tenantIdentifier, jobKey, triggerType, trigger, context);
|
|
||||||
}
|
}
|
||||||
return vetoJob;
|
return vetoJob;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void triggerMisfired(final Trigger trigger) {
|
public void triggerMisfired(final Trigger trigger) {
|
||||||
LOG.error("triggerMisfired() trigger={}", trigger);
|
log.error("triggerMisfired() trigger={}", trigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void triggerComplete(Trigger trigger, JobExecutionContext context, CompletedExecutionInstruction triggerInstructionCode) {
|
public void triggerComplete(Trigger trigger, JobExecutionContext context, CompletedExecutionInstruction triggerInstructionCode) {
|
||||||
LOG.debug("triggerComplete() trigger={}, context={}, completedExecutionInstruction={}", trigger, context, triggerInstructionCode);
|
log.debug("triggerComplete() trigger={}, context={}, completedExecutionInstruction={}", trigger, context, triggerInstructionCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-18
@@ -37,7 +37,6 @@ public final class TenantMapper implements RowMapper<FineractPlatformTenant> {
|
|||||||
+ " ts.pool_max_active as poolMaxActive, ts.pool_min_idle as poolMinIdle, ts.pool_max_idle as poolMaxIdle,"
|
+ " ts.pool_max_active as poolMaxActive, ts.pool_min_idle as poolMinIdle, ts.pool_max_idle as poolMaxIdle,"
|
||||||
+ " ts.pool_suspect_timeout as poolSuspectTimeout, ts.pool_time_between_eviction_runs_millis as poolTimeBetweenEvictionRunsMillis,"
|
+ " ts.pool_suspect_timeout as poolSuspectTimeout, ts.pool_time_between_eviction_runs_millis as poolTimeBetweenEvictionRunsMillis,"
|
||||||
+ " ts.pool_min_evictable_idle_time_millis as poolMinEvictableIdleTimeMillis,"
|
+ " ts.pool_min_evictable_idle_time_millis as poolMinEvictableIdleTimeMillis,"
|
||||||
+ " ts.deadlock_max_retries as maxRetriesOnDeadlock," + " ts.deadlock_max_retry_interval as maxIntervalBetweenRetries,"
|
|
||||||
+ " ts.readonly_schema_server as readOnlySchemaServer, " + " ts.readonly_schema_server_port as readOnlySchemaServerPort, "
|
+ " ts.readonly_schema_server as readOnlySchemaServer, " + " ts.readonly_schema_server_port as readOnlySchemaServerPort, "
|
||||||
+ " ts.readonly_schema_name as readOnlySchemaName, " + " ts.readonly_schema_username as readOnlySchemaUsername, "
|
+ " ts.readonly_schema_name as readOnlySchemaName, " + " ts.readonly_schema_username as readOnlySchemaUsername, "
|
||||||
+ " ts.readonly_schema_password as readOnlySchemaPassword, "
|
+ " ts.readonly_schema_password as readOnlySchemaPassword, "
|
||||||
@@ -99,26 +98,11 @@ public final class TenantMapper implements RowMapper<FineractPlatformTenant> {
|
|||||||
final int suspectTimeout = rs.getInt("poolSuspectTimeout");
|
final int suspectTimeout = rs.getInt("poolSuspectTimeout");
|
||||||
final int timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
|
final int timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
|
||||||
final int minEvictableIdleTimeMillis = rs.getInt("poolMinEvictableIdleTimeMillis");
|
final int minEvictableIdleTimeMillis = rs.getInt("poolMinEvictableIdleTimeMillis");
|
||||||
int maxRetriesOnDeadlock = rs.getInt("maxRetriesOnDeadlock");
|
|
||||||
int maxIntervalBetweenRetries = rs.getInt("maxIntervalBetweenRetries");
|
|
||||||
|
|
||||||
maxRetriesOnDeadlock = bindValueInMinMaxRange(maxRetriesOnDeadlock, 0, 15);
|
|
||||||
maxIntervalBetweenRetries = bindValueInMinMaxRange(maxIntervalBetweenRetries, 1, 15);
|
|
||||||
|
|
||||||
return new FineractPlatformTenantConnection(connectionId, schemaName, schemaServer, schemaServerPort, schemaConnectionParameters,
|
return new FineractPlatformTenantConnection(connectionId, schemaName, schemaServer, schemaServerPort, schemaConnectionParameters,
|
||||||
schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval, removeAbandoned, removeAbandonedTimeout,
|
schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval, removeAbandoned, removeAbandonedTimeout,
|
||||||
logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle, suspectTimeout, timeBetweenEvictionRunsMillis,
|
logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle, suspectTimeout, timeBetweenEvictionRunsMillis,
|
||||||
minEvictableIdleTimeMillis, maxRetriesOnDeadlock, maxIntervalBetweenRetries, testOnBorrow, readOnlySchemaServer,
|
minEvictableIdleTimeMillis, testOnBorrow, readOnlySchemaServer, readOnlySchemaServerPort, readOnlySchemaName,
|
||||||
readOnlySchemaServerPort, readOnlySchemaName, readOnlySchemaUsername, readOnlySchemaPassword,
|
readOnlySchemaUsername, readOnlySchemaPassword, readOnlySchemaConnectionParameters);
|
||||||
readOnlySchemaConnectionParameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int bindValueInMinMaxRange(final int value, int min, int max) {
|
|
||||||
if (value < min) {
|
|
||||||
return min;
|
|
||||||
} else if (value > max) {
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-38
@@ -18,7 +18,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.jobs.recalculateinterestforloan;
|
package org.apache.fineract.portfolio.loanaccount.jobs.recalculateinterestforloan;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -32,7 +31,6 @@ import java.util.concurrent.Future;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.collections.CollectionUtils;
|
import org.apache.commons.collections.CollectionUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
|
||||||
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
||||||
import org.apache.fineract.organisation.office.data.OfficeData;
|
import org.apache.fineract.organisation.office.data.OfficeData;
|
||||||
import org.apache.fineract.organisation.office.exception.OfficeNotFoundException;
|
import org.apache.fineract.organisation.office.exception.OfficeNotFoundException;
|
||||||
@@ -45,8 +43,6 @@ import org.springframework.batch.core.StepContribution;
|
|||||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||||
import org.springframework.batch.repeat.RepeatStatus;
|
import org.springframework.batch.repeat.RepeatStatus;
|
||||||
import org.springframework.dao.CannotAcquireLockException;
|
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -56,7 +52,6 @@ public class RecalculateInterestForLoanTasklet implements Tasklet {
|
|||||||
private final LoanWritePlatformService loanWritePlatformService;
|
private final LoanWritePlatformService loanWritePlatformService;
|
||||||
private final RecalculateInterestPoster recalculateInterestPoster;
|
private final RecalculateInterestPoster recalculateInterestPoster;
|
||||||
private final OfficeReadPlatformService officeReadPlatformService;
|
private final OfficeReadPlatformService officeReadPlatformService;
|
||||||
private static final SecureRandom random = new SecureRandom();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||||
@@ -75,45 +70,16 @@ public class RecalculateInterestForLoanTasklet implements Tasklet {
|
|||||||
|
|
||||||
recalculateInterest(office, threadPoolSize, batchSize);
|
recalculateInterest(office, threadPoolSize, batchSize);
|
||||||
} else {
|
} else {
|
||||||
int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
|
||||||
int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
|
|
||||||
Collection<Long> loanIds = loanReadPlatformService.fetchLoansForInterestRecalculation();
|
Collection<Long> loanIds = loanReadPlatformService.fetchLoansForInterestRecalculation();
|
||||||
int i = 0;
|
|
||||||
if (!loanIds.isEmpty()) {
|
if (!loanIds.isEmpty()) {
|
||||||
List<Throwable> errors = new ArrayList<>();
|
List<Throwable> errors = new ArrayList<>();
|
||||||
for (Long loanId : loanIds) {
|
for (Long loanId : loanIds) {
|
||||||
log.debug("recalculateInterest: Loan ID = {}", loanId);
|
log.debug("recalculateInterest: Loan ID = {}", loanId);
|
||||||
int numberOfRetries = 0;
|
try {
|
||||||
while (numberOfRetries <= maxNumberOfRetries) {
|
loanWritePlatformService.recalculateInterest(loanId);
|
||||||
try {
|
} catch (Exception e) {
|
||||||
loanWritePlatformService.recalculateInterest(loanId);
|
errors.add(e);
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
|
|
||||||
log.debug("Recalulate interest job has been retried {} time(s)", numberOfRetries);
|
|
||||||
if (numberOfRetries >= maxNumberOfRetries) {
|
|
||||||
log.error(
|
|
||||||
"Recalulate interest job has been retried for the max allowed attempts of {} and will be rolled back",
|
|
||||||
numberOfRetries);
|
|
||||||
errors.add(exception);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
int randomNum = random.nextInt(maxIntervalBetweenRetries + 1);
|
|
||||||
Thread.sleep(1000 + (randomNum * 1000L));
|
|
||||||
numberOfRetries = numberOfRetries + 1;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.error("Interest recalculation for loans retry failed due to InterruptedException", e);
|
|
||||||
errors.add(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Interest recalculation for loans failed for account {}", loanId, e);
|
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
errors.add(e);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
log.debug("recalculateInterest: Loans count {}", i);
|
|
||||||
}
|
}
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
throw new JobExecutionException(errors);
|
throw new JobExecutionException(errors);
|
||||||
|
|||||||
+14
-2
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
|||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import io.github.resilience4j.retry.annotation.Retry;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -39,7 +40,6 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||||
import org.apache.fineract.cob.domain.LoanAccountLockRepository;
|
|
||||||
import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException;
|
import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException;
|
||||||
import org.apache.fineract.cob.service.LoanAccountLockService;
|
import org.apache.fineract.cob.service.LoanAccountLockService;
|
||||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||||
@@ -281,7 +281,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
private final PostDatedChecksRepository postDatedChecksRepository;
|
private final PostDatedChecksRepository postDatedChecksRepository;
|
||||||
private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository;
|
private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository;
|
||||||
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
|
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
|
||||||
private final LoanAccountLockRepository loanAccountLockRepository;
|
|
||||||
private final LoanAccountLockService loanAccountLockService;
|
private final LoanAccountLockService loanAccountLockService;
|
||||||
|
|
||||||
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
||||||
@@ -3217,6 +3216,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
|
@Retry(name = "recalculateInterest", fallbackMethod = "fallbackRecalculateInterest")
|
||||||
public void recalculateInterest(final long loanId) {
|
public void recalculateInterest(final long loanId) {
|
||||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||||
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
|
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
|
||||||
@@ -3252,6 +3252,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
|||||||
return new CommandProcessingResultBuilder().withLoanId(loanId).build();
|
return new CommandProcessingResultBuilder().withLoanId(loanId).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private void fallbackRecalculateInterest(Throwable t) {
|
||||||
|
// NOTE: allow caller to catch the exceptions
|
||||||
|
|
||||||
|
if (t instanceof RuntimeException re) {
|
||||||
|
throw re;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: wrap throwable only if really necessary
|
||||||
|
throw new RuntimeException(t);
|
||||||
|
}
|
||||||
|
|
||||||
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
|
private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) {
|
||||||
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
|
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
|
||||||
if (transferTransaction != null) {
|
if (transferTransaction != null) {
|
||||||
|
|||||||
+4
-46
@@ -18,19 +18,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.service;
|
package org.apache.fineract.portfolio.loanaccount.service;
|
||||||
|
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
|
||||||
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.dao.CannotAcquireLockException;
|
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -38,7 +33,6 @@ import org.springframework.stereotype.Component;
|
|||||||
public class RecalculateInterestPoster implements Callable<Void> {
|
public class RecalculateInterestPoster implements Callable<Void> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(RecalculateInterestPoster.class);
|
private static final Logger LOG = LoggerFactory.getLogger(RecalculateInterestPoster.class);
|
||||||
private static final SecureRandom random = new SecureRandom();
|
|
||||||
|
|
||||||
private Collection<Long> loanIds;
|
private Collection<Long> loanIds;
|
||||||
private LoanWritePlatformService loanWritePlatformService;
|
private LoanWritePlatformService loanWritePlatformService;
|
||||||
@@ -52,52 +46,16 @@ public class RecalculateInterestPoster implements Callable<Void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressFBWarnings(value = {
|
|
||||||
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
|
||||||
public Void call() throws JobExecutionException {
|
public Void call() throws JobExecutionException {
|
||||||
Integer maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
|
||||||
Integer maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
if (!loanIds.isEmpty()) {
|
if (!loanIds.isEmpty()) {
|
||||||
List<Throwable> errors = new ArrayList<>();
|
List<Throwable> errors = new ArrayList<>();
|
||||||
for (Long loanId : loanIds) {
|
for (Long loanId : loanIds) {
|
||||||
LOG.debug("Loan ID {}", loanId);
|
LOG.debug("Loan ID {}", loanId);
|
||||||
Integer numberOfRetries = 0;
|
try {
|
||||||
while (numberOfRetries <= maxNumberOfRetries) {
|
loanWritePlatformService.recalculateInterest(loanId);
|
||||||
try {
|
} catch (Exception e) {
|
||||||
this.loanWritePlatformService.recalculateInterest(loanId);
|
errors.add(e);
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
|
|
||||||
LOG.debug("Recalculate interest job has been retried {} time(s)", numberOfRetries);
|
|
||||||
// Fail if the transaction has been retired for
|
|
||||||
// maxNumberOfRetries
|
|
||||||
if (numberOfRetries >= maxNumberOfRetries) {
|
|
||||||
LOG.error(
|
|
||||||
"Recalculate interest job has been retried for the max allowed attempts of {} and will be rolled back",
|
|
||||||
numberOfRetries);
|
|
||||||
errors.add(exception);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Else sleep for a random time (between 1 to 10
|
|
||||||
// seconds) and continue
|
|
||||||
try {
|
|
||||||
int randomNum = random.nextInt(maxIntervalBetweenRetries + 1);
|
|
||||||
Thread.sleep(1000 + (randomNum * 1000));
|
|
||||||
numberOfRetries = numberOfRetries + 1;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOG.error("Interest recalculation for loans retry failed due to InterruptedException", e);
|
|
||||||
errors.add(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Interest recalculation for loans failed for account {}", loanId, e);
|
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
errors.add(e);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
LOG.debug("Loans count {}", i);
|
|
||||||
}
|
}
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
throw new JobExecutionException(errors);
|
throw new JobExecutionException(errors);
|
||||||
|
|||||||
+3
-23
@@ -36,21 +36,15 @@ import org.apache.commons.collections4.CollectionUtils;
|
|||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractContext;
|
import org.apache.fineract.infrastructure.core.domain.FineractContext;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService;
|
|
||||||
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster;
|
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster;
|
||||||
import org.springframework.batch.core.StepContribution;
|
import org.springframework.batch.core.StepContribution;
|
||||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||||
import org.springframework.batch.repeat.RepeatStatus;
|
import org.springframework.batch.repeat.RepeatStatus;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -59,15 +53,9 @@ public class PostInterestForSavingTasklet implements Tasklet {
|
|||||||
|
|
||||||
private final SavingsAccountReadPlatformService savingAccountReadPlatformService;
|
private final SavingsAccountReadPlatformService savingAccountReadPlatformService;
|
||||||
private final ConfigurationDomainService configurationDomainService;
|
private final ConfigurationDomainService configurationDomainService;
|
||||||
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
|
|
||||||
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
|
|
||||||
private final SavingsAccountAssembler savingAccountAssembler;
|
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final TransactionTemplate transactionTemplate;
|
|
||||||
private final Queue<List<SavingsAccountData>> queue = new ArrayDeque<>();
|
private final Queue<List<SavingsAccountData>> queue = new ArrayDeque<>();
|
||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
private final int queueSize = 1;
|
private final int queueSize = 1;
|
||||||
private final PlatformSecurityContext securityContext;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||||
@@ -147,19 +135,11 @@ public class PostInterestForSavingTasklet implements Tasklet {
|
|||||||
|
|
||||||
for (long i = 0; i < loopCount; i++) {
|
for (long i = 0; i < loopCount; i++) {
|
||||||
List<SavingsAccountData> subList = safeSubList(savingsAccounts, fromIndex, toIndex);
|
List<SavingsAccountData> subList = safeSubList(savingsAccounts, fromIndex, toIndex);
|
||||||
SavingsSchedularInterestPoster savingsSchedularInterestPoster = (SavingsSchedularInterestPoster) applicationContext
|
SavingsSchedularInterestPoster savingsSchedularInterestPoster = applicationContext
|
||||||
.getBean("savingsSchedularInterestPoster");
|
.getBean(SavingsSchedularInterestPoster.class);
|
||||||
savingsSchedularInterestPoster.setSavingAccounts(subList);
|
savingsSchedularInterestPoster.setSavingAccounts(subList);
|
||||||
savingsSchedularInterestPoster.setContext(ThreadLocalContextUtil.getContext());
|
|
||||||
savingsSchedularInterestPoster.setSavingsAccountWritePlatformService(savingsAccountWritePlatformService);
|
|
||||||
savingsSchedularInterestPoster.setSavingsAccountReadPlatformService(savingAccountReadPlatformService);
|
|
||||||
savingsSchedularInterestPoster.setSavingsAccountRepository(savingsAccountRepository);
|
|
||||||
savingsSchedularInterestPoster.setSavingAccountAssembler(savingAccountAssembler);
|
|
||||||
savingsSchedularInterestPoster.setJdbcTemplate(jdbcTemplate);
|
|
||||||
savingsSchedularInterestPoster.setBackdatedTxnsAllowedTill(backdatedTxnsAllowedTill);
|
savingsSchedularInterestPoster.setBackdatedTxnsAllowedTill(backdatedTxnsAllowedTill);
|
||||||
savingsSchedularInterestPoster.setTransactionTemplate(transactionTemplate);
|
savingsSchedularInterestPoster.setContext(ThreadLocalContextUtil.getContext());
|
||||||
savingsSchedularInterestPoster.setConfigurationDomainService(configurationDomainService);
|
|
||||||
savingsSchedularInterestPoster.setPlatformSecurityContext(securityContext);
|
|
||||||
|
|
||||||
posters.add(savingsSchedularInterestPoster);
|
posters.add(savingsSchedularInterestPoster);
|
||||||
|
|
||||||
|
|||||||
+24
-71
@@ -31,6 +31,7 @@ import static org.apache.fineract.portfolio.savings.SavingsApiConstants.withdraw
|
|||||||
|
|
||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
|
import io.github.resilience4j.retry.annotation.Retry;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.MathContext;
|
import java.math.MathContext;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
@@ -46,6 +47,8 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
@@ -68,7 +71,6 @@ import org.apache.fineract.infrastructure.event.business.domain.savings.SavingsP
|
|||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
|
import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper;
|
||||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
|
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||||
import org.apache.fineract.organisation.office.domain.Office;
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
@@ -125,18 +127,16 @@ import org.apache.fineract.portfolio.savings.exception.TransactionUpdateNotAllow
|
|||||||
import org.apache.fineract.portfolio.transfer.api.TransferApiConstants;
|
import org.apache.fineract.portfolio.transfer.api.TransferApiConstants;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
|
import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Propagation;
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Service
|
@Service
|
||||||
public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService {
|
public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService {
|
||||||
|
|
||||||
@@ -149,7 +149,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator;
|
private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator;
|
||||||
private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator;
|
private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator;
|
||||||
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
|
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
|
||||||
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
|
|
||||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||||
private final SavingsAccountDomainService savingsAccountDomainService;
|
private final SavingsAccountDomainService savingsAccountDomainService;
|
||||||
private final NoteRepository noteRepository;
|
private final NoteRepository noteRepository;
|
||||||
@@ -166,63 +165,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
private final StandingInstructionRepository standingInstructionRepository;
|
private final StandingInstructionRepository standingInstructionRepository;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final GSIMRepositoy gsimRepository;
|
private final GSIMRepositoy gsimRepository;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
|
||||||
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
private final SavingsAccountInterestPostingService savingsAccountInterestPostingService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public SavingsAccountWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
|
|
||||||
final SavingsAccountRepositoryWrapper savingAccountRepositoryWrapper,
|
|
||||||
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
|
|
||||||
final SavingsAccountAssembler savingAccountAssembler,
|
|
||||||
final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator,
|
|
||||||
final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator,
|
|
||||||
final PaymentDetailWritePlatformService paymentDetailWritePlatformService,
|
|
||||||
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
|
|
||||||
final JournalEntryWritePlatformService journalEntryWritePlatformService,
|
|
||||||
final SavingsAccountDomainService savingsAccountDomainService, final NoteRepository noteRepository,
|
|
||||||
final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final HolidayRepositoryWrapper holidayRepository,
|
|
||||||
final WorkingDaysRepositoryWrapper workingDaysRepository,
|
|
||||||
final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService,
|
|
||||||
final ChargeRepositoryWrapper chargeRepository, final SavingsAccountChargeRepositoryWrapper savingsAccountChargeRepository,
|
|
||||||
final SavingsAccountDataValidator fromApiJsonDeserializer, final StaffRepositoryWrapper staffRepository,
|
|
||||||
final ConfigurationDomainService configurationDomainService,
|
|
||||||
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
|
||||||
final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService,
|
|
||||||
final AppUserRepositoryWrapper appuserRepository, final StandingInstructionRepository standingInstructionRepository,
|
|
||||||
final BusinessEventNotifierService businessEventNotifierService, final GSIMRepositoy gsimRepository,
|
|
||||||
final JdbcTemplate jdbcTemplate, final SavingsAccountInterestPostingService savingsAccountInterestPostingService) {
|
|
||||||
this.context = context;
|
|
||||||
this.savingAccountRepositoryWrapper = savingAccountRepositoryWrapper;
|
|
||||||
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
|
|
||||||
this.savingAccountAssembler = savingAccountAssembler;
|
|
||||||
this.savingsAccountTransactionDataValidator = savingsAccountTransactionDataValidator;
|
|
||||||
this.savingsAccountChargeDataValidator = savingsAccountChargeDataValidator;
|
|
||||||
this.paymentDetailWritePlatformService = paymentDetailWritePlatformService;
|
|
||||||
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
|
|
||||||
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
|
|
||||||
this.savingsAccountDomainService = savingsAccountDomainService;
|
|
||||||
this.noteRepository = noteRepository;
|
|
||||||
this.accountTransfersReadPlatformService = accountTransfersReadPlatformService;
|
|
||||||
this.accountAssociationsReadPlatformService = accountAssociationsReadPlatformService;
|
|
||||||
this.chargeRepository = chargeRepository;
|
|
||||||
this.savingsAccountChargeRepository = savingsAccountChargeRepository;
|
|
||||||
this.holidayRepository = holidayRepository;
|
|
||||||
this.workingDaysRepository = workingDaysRepository;
|
|
||||||
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
|
|
||||||
this.staffRepository = staffRepository;
|
|
||||||
this.configurationDomainService = configurationDomainService;
|
|
||||||
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
|
|
||||||
this.entityDatatableChecksWritePlatformService = entityDatatableChecksWritePlatformService;
|
|
||||||
this.appuserRepository = appuserRepository;
|
|
||||||
this.standingInstructionRepository = standingInstructionRepository;
|
|
||||||
this.businessEventNotifierService = businessEventNotifierService;
|
|
||||||
this.gsimRepository = gsimRepository;
|
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
|
||||||
this.savingsAccountInterestPostingService = savingsAccountInterestPostingService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SavingsAccountWritePlatformServiceJpaRepositoryImpl.class);
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) {
|
public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) {
|
||||||
@@ -355,7 +299,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
|
|
||||||
if (account.getGsim() != null) {
|
if (account.getGsim() != null) {
|
||||||
isGsim = true;
|
isGsim = true;
|
||||||
LOG.debug("is gsim");
|
log.debug("is gsim");
|
||||||
}
|
}
|
||||||
checkClientOrGroupActive(account);
|
checkClientOrGroupActive(account);
|
||||||
|
|
||||||
@@ -376,16 +320,16 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
|
|
||||||
if (isGsim && (deposit.getId() != null)) {
|
if (isGsim && (deposit.getId() != null)) {
|
||||||
|
|
||||||
LOG.debug("Deposit account has been created: {} ", deposit);
|
log.debug("Deposit account has been created: {} ", deposit);
|
||||||
|
|
||||||
GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow();
|
GroupSavingsIndividualMonitoring gsim = gsimRepository.findById(account.getGsim().getId()).orElseThrow();
|
||||||
LOG.debug("parent deposit : {} ", gsim.getParentDeposit());
|
log.debug("parent deposit : {} ", gsim.getParentDeposit());
|
||||||
LOG.debug("child account : {} ", savingsId);
|
log.debug("child account : {} ", savingsId);
|
||||||
BigDecimal currentBalance = gsim.getParentDeposit();
|
BigDecimal currentBalance = gsim.getParentDeposit();
|
||||||
BigDecimal newBalance = currentBalance.add(transactionAmount);
|
BigDecimal newBalance = currentBalance.add(transactionAmount);
|
||||||
gsim.setParentDeposit(newBalance);
|
gsim.setParentDeposit(newBalance);
|
||||||
gsimRepository.save(gsim);
|
gsimRepository.save(gsim);
|
||||||
LOG.debug("balance after making deposit : {} ",
|
log.debug("balance after making deposit : {} ",
|
||||||
gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit());
|
gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit());
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -641,6 +585,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
|
@Retry(name = "postInterest", fallbackMethod = "fallbackPostInterest")
|
||||||
public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs,
|
public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs,
|
||||||
final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) {
|
final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) {
|
||||||
|
|
||||||
@@ -662,14 +607,9 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
postInterestOnDate = transactionDate;
|
postInterestOnDate = transactionDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
long startPosting = System.currentTimeMillis();
|
|
||||||
LOG.debug("Interest Posting Start Here at {}", startPosting);
|
|
||||||
|
|
||||||
savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer,
|
savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer,
|
||||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
|
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
|
||||||
savingsAccountData);
|
savingsAccountData);
|
||||||
long endPosting = System.currentTimeMillis();
|
|
||||||
LOG.debug("Interest Posting Ends within {}", endPosting - startPosting);
|
|
||||||
|
|
||||||
if (!backdatedTxnsAllowedTill) {
|
if (!backdatedTxnsAllowedTill) {
|
||||||
List<SavingsAccountTransactionData> transactions = savingsAccountData.getSavingsAccountTransactionData();
|
List<SavingsAccountTransactionData> transactions = savingsAccountData.getSavingsAccountTransactionData();
|
||||||
@@ -1459,6 +1399,19 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private SavingsAccountData fallbackPostInterest(SavingsAccountData savingsAccountData, boolean postInterestAs,
|
||||||
|
LocalDate transactionDate, boolean backdatedTxnsAllowedTill, Throwable t) {
|
||||||
|
// NOTE: allow caller to catch the exceptions
|
||||||
|
|
||||||
|
if (t instanceof RuntimeException re) {
|
||||||
|
throw re;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: wrap throwable only if really necessary
|
||||||
|
throw new RuntimeException(t);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate,
|
private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate,
|
||||||
final BigDecimal amountPaid, final DateTimeFormatter formatter, final AppUser user, final boolean backdatedTxnsAllowedTill) {
|
final BigDecimal amountPaid, final DateTimeFormatter formatter, final AppUser user, final boolean backdatedTxnsAllowedTill) {
|
||||||
|
|||||||
+23
-87
@@ -18,9 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.savings.service;
|
package org.apache.fineract.portfolio.savings.service;
|
||||||
|
|
||||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -30,11 +28,10 @@ import java.util.HashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
|
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
|
||||||
import org.apache.fineract.batch.command.CommandStrategyProvider;
|
|
||||||
import org.apache.fineract.batch.service.ResolutionHelper;
|
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractContext;
|
import org.apache.fineract.infrastructure.core.domain.FineractContext;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
@@ -43,119 +40,61 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
|||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountSummaryData;
|
||||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
|
|
||||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.dao.CannotAcquireLockException;
|
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Isolation;
|
import org.springframework.transaction.annotation.Isolation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author manoj
|
* @author manoj
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Setter
|
||||||
@Component
|
@Component
|
||||||
@Scope("prototype")
|
@Scope("prototype")
|
||||||
@Setter
|
|
||||||
public class SavingsSchedularInterestPoster implements Callable<Void> {
|
public class SavingsSchedularInterestPoster implements Callable<Void> {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(SavingsSchedularInterestPoster.class);
|
|
||||||
private static final SecureRandom random = new SecureRandom();
|
|
||||||
private static final String SAVINGS_TRANSACTION_IDENTIFIER = "S";
|
private static final String SAVINGS_TRANSACTION_IDENTIFIER = "S";
|
||||||
|
|
||||||
|
private final SavingsAccountWritePlatformService savingsAccountWritePlatformService;
|
||||||
|
private final List<SavingsAccountData> savingsAccountDataList = new ArrayList<>();
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
|
||||||
|
private final PlatformSecurityContext platformSecurityContext;
|
||||||
private Collection<SavingsAccountData> savingAccounts;
|
private Collection<SavingsAccountData> savingAccounts;
|
||||||
private SavingsAccountWritePlatformService savingsAccountWritePlatformService;
|
|
||||||
private SavingsAccountRepositoryWrapper savingsAccountRepository;
|
|
||||||
private SavingsAccountAssembler savingAccountAssembler;
|
|
||||||
private FineractContext context;
|
private FineractContext context;
|
||||||
private ConfigurationDomainService configurationDomainService;
|
|
||||||
private boolean backdatedTxnsAllowedTill;
|
private boolean backdatedTxnsAllowedTill;
|
||||||
private List<SavingsAccountData> savingsAccountDataList = new ArrayList<>();
|
|
||||||
private JdbcTemplate jdbcTemplate;
|
|
||||||
private TransactionTemplate transactionTemplate;
|
|
||||||
private CommandStrategyProvider strategyProvider;
|
|
||||||
private ResolutionHelper resolutionHelper;
|
|
||||||
private SavingsAccountReadPlatformService savingsAccountReadPlatformService;
|
|
||||||
private PlatformSecurityContext platformSecurityContext;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressFBWarnings(value = {
|
|
||||||
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
|
||||||
@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
|
@Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class)
|
||||||
public Void call() throws org.apache.fineract.infrastructure.jobs.exception.JobExecutionException {
|
public Void call() throws org.apache.fineract.infrastructure.jobs.exception.JobExecutionException {
|
||||||
ThreadLocalContextUtil.init(this.context);
|
ThreadLocalContextUtil.init(this.context);
|
||||||
Integer maxNumberOfRetries = this.context.getTenantContext().getConnection().getMaxRetriesOnDeadlock();
|
|
||||||
Integer maxIntervalBetweenRetries = this.context.getTenantContext().getConnection().getMaxIntervalBetweenRetries();
|
|
||||||
|
|
||||||
// List<BatchResponse> responseList = new ArrayList<>();
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
LOG.debug("Thread Execution Started at {}", start);
|
|
||||||
|
|
||||||
List<Throwable> errors = new ArrayList<>();
|
|
||||||
int i = 0;
|
|
||||||
if (!savingAccounts.isEmpty()) {
|
if (!savingAccounts.isEmpty()) {
|
||||||
|
List<Throwable> errors = new ArrayList<>();
|
||||||
for (SavingsAccountData savingsAccountData : savingAccounts) {
|
for (SavingsAccountData savingsAccountData : savingAccounts) {
|
||||||
Integer numberOfRetries = 0;
|
boolean postInterestAsOn = false;
|
||||||
while (numberOfRetries <= maxNumberOfRetries) {
|
LocalDate transactionDate = null;
|
||||||
try {
|
try {
|
||||||
boolean postInterestAsOn = false;
|
SavingsAccountData savingsAccountDataRet = savingsAccountWritePlatformService.postInterest(savingsAccountData,
|
||||||
LocalDate transactionDate = null;
|
postInterestAsOn, transactionDate, backdatedTxnsAllowedTill);
|
||||||
long startPosting = System.currentTimeMillis();
|
savingsAccountDataList.add(savingsAccountDataRet);
|
||||||
SavingsAccountData savingsAccountDataRet = savingsAccountWritePlatformService.postInterest(savingsAccountData,
|
} catch (Exception e) {
|
||||||
postInterestAsOn, transactionDate, backdatedTxnsAllowedTill);
|
errors.add(e);
|
||||||
long endPosting = System.currentTimeMillis();
|
|
||||||
savingsAccountDataList.add(savingsAccountDataRet);
|
|
||||||
|
|
||||||
LOG.debug("Posting Completed Within {}", endPosting - startPosting);
|
|
||||||
|
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
} catch (CannotAcquireLockException | ObjectOptimisticLockingFailureException exception) {
|
|
||||||
LOG.debug("Interest posting job for savings ID {} has been retried {} time(s)", savingsAccountData.getId(),
|
|
||||||
numberOfRetries);
|
|
||||||
// Fail if the transaction has been retired for
|
|
||||||
// maxNumberOfRetries
|
|
||||||
if (numberOfRetries >= maxNumberOfRetries) {
|
|
||||||
LOG.error(
|
|
||||||
"Interest posting job for savings ID {} has been retried for the max allowed attempts of {} and will be rolled back",
|
|
||||||
savingsAccountData.getId(), numberOfRetries);
|
|
||||||
errors.add(exception);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Else sleep for a random time (between 1 to 10
|
|
||||||
// seconds) and continue
|
|
||||||
try {
|
|
||||||
int randomNum = random.nextInt(maxIntervalBetweenRetries + 1);
|
|
||||||
Thread.sleep(1000 + (randomNum * 1000));
|
|
||||||
numberOfRetries = numberOfRetries + 1;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
LOG.error("Interest posting job for savings retry failed due to InterruptedException", e);
|
|
||||||
errors.add(e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.error("Interest posting job for savings failed for account {}", savingsAccountData.getId(), e);
|
|
||||||
numberOfRetries = maxNumberOfRetries + 1;
|
|
||||||
errors.add(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors.isEmpty()) {
|
if (errors.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
batchUpdate(savingsAccountDataList);
|
batchUpdate(savingsAccountDataList);
|
||||||
} catch (DataAccessException exception) {
|
} catch (DataAccessException exception) {
|
||||||
LOG.error("Batch update failed due to DataAccessException", exception);
|
log.error("Batch update failed due to DataAccessException", exception);
|
||||||
errors.add(exception);
|
errors.add(exception);
|
||||||
} catch (NullPointerException exception) {
|
} catch (NullPointerException exception) {
|
||||||
LOG.error("Batch update failed due to NullPointerException", exception);
|
log.error("Batch update failed due to NullPointerException", exception);
|
||||||
errors.add(exception);
|
errors.add(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,9 +104,6 @@ public class SavingsSchedularInterestPoster implements Callable<Void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
long end = System.currentTimeMillis();
|
|
||||||
LOG.debug("Time To Finish the batch {} by thread {} for accounts {}", end - start, Thread.currentThread().getId(),
|
|
||||||
savingAccounts.size());
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,10 +233,10 @@ public class SavingsSchedularInterestPoster implements Callable<Void> {
|
|||||||
this.jdbcTemplate.batchUpdate(queryForSavingsUpdate, paramsForSavingsSummary);
|
this.jdbcTemplate.batchUpdate(queryForSavingsUpdate, paramsForSavingsSummary);
|
||||||
this.jdbcTemplate.batchUpdate(queryForTransactionInsertion, paramsForTransactionInsertion);
|
this.jdbcTemplate.batchUpdate(queryForTransactionInsertion, paramsForTransactionInsertion);
|
||||||
this.jdbcTemplate.batchUpdate(queryForTransactionUpdate, paramsForTransactionUpdate);
|
this.jdbcTemplate.batchUpdate(queryForTransactionUpdate, paramsForTransactionUpdate);
|
||||||
LOG.debug("`Total No Of Interest Posting:` {}", transRefNo.size());
|
log.debug("`Total No Of Interest Posting:` {}", transRefNo.size());
|
||||||
List<SavingsAccountTransactionData> savingsAccountTransactionDataList = fetchTransactionsFromIds(transRefNo);
|
List<SavingsAccountTransactionData> savingsAccountTransactionDataList = fetchTransactionsFromIds(transRefNo);
|
||||||
if (savingsAccountDataList != null) {
|
if (savingsAccountDataList != null) {
|
||||||
LOG.debug("Fetched Transactions from DB: {}", savingsAccountTransactionDataList.size());
|
log.debug("Fetched Transactions from DB: {}", savingsAccountTransactionDataList.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
HashMap<String, SavingsAccountTransactionData> savingsAccountTransactionMap = new HashMap<>();
|
HashMap<String, SavingsAccountTransactionData> savingsAccountTransactionMap = new HashMap<>();
|
||||||
|
|||||||
@@ -73,17 +73,18 @@ fineract.loan.transactionprocessor.error-not-found-fail=${FINERACT_LOAN_TRANSACT
|
|||||||
# Logging pattern for the console
|
# Logging pattern for the console
|
||||||
logging.pattern.console=${CONSOLE_LOG_PATTERN:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(%replace([%X{correlationId}]){'\\[\\]', ''}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}
|
logging.pattern.console=${CONSOLE_LOG_PATTERN:%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(%replace([%X{correlationId}]){'\\[\\]', ''}) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}}
|
||||||
|
|
||||||
management.health.jms.enabled=false
|
management.health.jms.enabled=${FINERACT_MANAGEMENT_HEALTH_JMS_ENABLED:false}
|
||||||
|
|
||||||
# FINERACT 1296
|
# FINERACT 1296
|
||||||
management.endpoint.health.probes.enabled=true
|
management.endpoint.health.probes.enabled=true
|
||||||
management.health.livenessState.enabled=true
|
management.health.livenessState.enabled=true
|
||||||
management.health.readinessState.enabled=true
|
management.health.readinessState.enabled=true
|
||||||
|
|
||||||
|
management.health.ratelimiters.enabled=${FINERACT_MANAGEMENT_HEALTH_RATELIMITERS_ENABLED:false}
|
||||||
|
|
||||||
# FINERACT-883
|
# FINERACT-883
|
||||||
management.info.git.mode=FULL
|
management.info.git.mode=FULL
|
||||||
management.endpoints.web.exposure.include=health,info
|
management.endpoints.web.exposure.include=${FINERACT_MANAGEMENT_ENDPOINT_WEB_EXPOSURE_INCLUDE:health,info}
|
||||||
|
|
||||||
# FINERACT-914
|
# FINERACT-914
|
||||||
server.forward-headers-strategy=framework
|
server.forward-headers-strategy=framework
|
||||||
server.port=${FINERACT_SERVER_PORT:8443}
|
server.port=${FINERACT_SERVER_PORT:8443}
|
||||||
@@ -164,3 +165,26 @@ spring.main.allow-bean-definition-overriding=true
|
|||||||
spring.batch.initialize-schema=NEVER
|
spring.batch.initialize-schema=NEVER
|
||||||
# Disabling Spring Batch jobs on startup
|
# Disabling Spring Batch jobs on startup
|
||||||
spring.batch.job.enabled=false
|
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.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException}
|
||||||
|
|
||||||
|
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}
|
||||||
|
resilience4j.retry.instances.processJobDetailForExecution.enable-exponential-backoff=${FINERACT_PROCESS_JOB_DETAIL_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
|
||||||
|
resilience4j.retry.instances.processJobDetailForExecution.exponential-backoff-multiplier=${FINERACT_PROCESS_JOB_DETAIL_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
|
||||||
|
|
||||||
|
resilience4j.retry.instances.recalculateInterest.max-attempts=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_MAX_ATTEMPTS:3}
|
||||||
|
resilience4j.retry.instances.recalculateInterest.wait-duration=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_WAIT_DURATION:1s}
|
||||||
|
resilience4j.retry.instances.recalculateInterest.enable-exponential-backoff=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
|
||||||
|
resilience4j.retry.instances.recalculateInterest.exponential-backoff-multiplier=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
|
||||||
|
resilience4j.retry.instances.recalculateInterest.retryExceptions=${FINERACT_PROCESS_RECALCULATE_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException}
|
||||||
|
|
||||||
|
resilience4j.retry.instances.postInterest.max-attempts=${FINERACT_PROCESS_POST_INTEREST_RETRY_MAX_ATTEMPTS:3}
|
||||||
|
resilience4j.retry.instances.postInterest.wait-duration=${FINERACT_PROCESS_POST_INTEREST_RETRY_WAIT_DURATION:1s}
|
||||||
|
resilience4j.retry.instances.postInterest.enable-exponential-backoff=${FINERACT_PROCESS_POST_INTEREST_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
|
||||||
|
resilience4j.retry.instances.postInterest.exponential-backoff-multiplier=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
|
||||||
|
resilience4j.retry.instances.postInterest.retryExceptions=${FINERACT_PROCESS_POST_INTEREST_RETRY_EXCEPTIONS:org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException}
|
||||||
|
|||||||
+1
@@ -25,4 +25,5 @@
|
|||||||
<include file="parts/0003_reset_postgresql_sequences.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0003_reset_postgresql_sequences.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0004_readonly_database_connection.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0004_readonly_database_connection.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0005_jdbc_connection_string.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0005_jdbc_connection_string.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0006_drop_retry_parameter_columns.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
<changeSet author="fineract" id="1" context="tenant_store_db">
|
||||||
|
<dropColumn tableName="tenant_server_connections" columnName="deadlock_max_retries" />
|
||||||
|
<dropColumn tableName="tenant_server_connections" columnName="deadlock_max_retry_interval" />
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* 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.commands.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
import io.cucumber.java8.En;
|
||||||
|
import io.github.resilience4j.retry.RetryRegistry;
|
||||||
|
import io.github.resilience4j.retry.event.RetryEvent;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
import org.apache.fineract.commands.domain.CommandSource;
|
||||||
|
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||||
|
import org.apache.fineract.commands.exception.RollbackTransactionAsCommandIsNotApprovedByCheckerException;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.dao.CannotAcquireLockException;
|
||||||
|
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||||
|
|
||||||
|
public class CommandServiceStepDefinitions implements En {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CommandServiceStepDefinitions.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CommandProcessingService processAndLogCommandService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RetryRegistry retryRegistry;
|
||||||
|
|
||||||
|
private PortfolioCommandSourceWritePlatformService commandSourceWritePlatformService;
|
||||||
|
|
||||||
|
private DummyCommand command;
|
||||||
|
|
||||||
|
private RetryEvent retryEvent;
|
||||||
|
|
||||||
|
public CommandServiceStepDefinitions() {
|
||||||
|
Given("/^A command source write service$/", () -> {
|
||||||
|
this.commandSourceWritePlatformService = new DummyCommandSourceWriteService(processAndLogCommandService);
|
||||||
|
this.command = new DummyCommand();
|
||||||
|
this.retryRegistry.retry("executeCommand").getEventPublisher().onRetry(event -> {
|
||||||
|
log.warn("... retry event: {}", event);
|
||||||
|
|
||||||
|
CommandServiceStepDefinitions.this.retryEvent = event;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
When("/^The user executes the command via a command write service with exceptions$/", () -> {
|
||||||
|
try {
|
||||||
|
this.commandSourceWritePlatformService.logCommandSource(command);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// TODO: this exception is OK for now; we need to fix the whole tenant based data source setup
|
||||||
|
log.warn("At the moment mocking data access is so incredibly hard... it's easier to just ignore this exception: {}",
|
||||||
|
e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Then("/^The command processing service should fallback as expected$/", () -> {
|
||||||
|
assertNotNull(retryEvent);
|
||||||
|
assertEquals("executeCommand", retryEvent.getName());
|
||||||
|
assertEquals(2, retryEvent.getNumberOfRetryAttempts());
|
||||||
|
});
|
||||||
|
|
||||||
|
Then("/^The command processing service execute function should be called 3 times$/", () -> {
|
||||||
|
assertEquals(3, command.getCount());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DummyCommand extends CommandWrapper {
|
||||||
|
|
||||||
|
private AtomicInteger counter = new AtomicInteger();
|
||||||
|
|
||||||
|
public DummyCommand() {
|
||||||
|
super(null, null, null, null, null, null, null, null, null, null, "{}", null, null, null, null, null, null,
|
||||||
|
UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String taskPermissionName() {
|
||||||
|
// NOTE: simulating a failure scenario that triggers retries; using this function, because it is the first
|
||||||
|
// called in the command processing service
|
||||||
|
|
||||||
|
int step = counter.incrementAndGet();
|
||||||
|
|
||||||
|
log.warn("Round: {}", step);
|
||||||
|
|
||||||
|
if (step == 1) {
|
||||||
|
throw new CannotAcquireLockException("BLOW IT UP!!!");
|
||||||
|
} else if (step == 2) {
|
||||||
|
throw new ObjectOptimisticLockingFailureException("Dummy", new RuntimeException("BLOW IT UP!!!"));
|
||||||
|
} else if (step == 3) {
|
||||||
|
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(new DummyCommandSource());
|
||||||
|
}
|
||||||
|
|
||||||
|
return "dummy";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String actionName() {
|
||||||
|
return "dummy";
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return counter.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DummyCommandSourceWriteService implements PortfolioCommandSourceWritePlatformService {
|
||||||
|
|
||||||
|
private final CommandProcessingService processAndLogCommandService;
|
||||||
|
|
||||||
|
public DummyCommandSourceWriteService(CommandProcessingService processAndLogCommandService) {
|
||||||
|
this.processAndLogCommandService = processAndLogCommandService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult logCommandSource(CommandWrapper wrapper) {
|
||||||
|
final String json = wrapper.getJson();
|
||||||
|
JsonCommand command = JsonCommand.from(json, null, null, wrapper.getEntityName(), wrapper.getEntityId(),
|
||||||
|
wrapper.getSubentityId(), wrapper.getGroupId(), wrapper.getClientId(), wrapper.getLoanId(), wrapper.getSavingsId(),
|
||||||
|
wrapper.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
|
||||||
|
wrapper.getOrganisationCreditBureauId(), wrapper.getJobName());
|
||||||
|
|
||||||
|
return this.processAndLogCommandService.executeCommand(wrapper, command, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult approveEntry(Long id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long rejectEntry(Long id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long deleteEntry(Long makerCheckerId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_portfolio_command_source")
|
||||||
|
public static class DummyCommandSource extends CommandSource {
|
||||||
|
|
||||||
|
public DummyCommandSource() {
|
||||||
|
setId(1L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -111,3 +111,26 @@ spring.main.allow-bean-definition-overriding=true
|
|||||||
spring.batch.initialize-schema=NEVER
|
spring.batch.initialize-schema=NEVER
|
||||||
# Disabling Spring Batch jobs on startup
|
# Disabling Spring Batch jobs on startup
|
||||||
spring.batch.job.enabled=false
|
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
|
||||||
|
|
||||||
|
resilience4j.retry.instances.processJobDetailForExecution.max-attempts=3
|
||||||
|
resilience4j.retry.instances.processJobDetailForExecution.wait-duration=1s
|
||||||
|
resilience4j.retry.instances.processJobDetailForExecution.enable-exponential-backoff=true
|
||||||
|
resilience4j.retry.instances.processJobDetailForExecution.exponential-backoff-multiplier=2
|
||||||
|
|
||||||
|
resilience4j.retry.instances.recalculateInterest.max-attempts=3
|
||||||
|
resilience4j.retry.instances.recalculateInterest.wait-duration=1s
|
||||||
|
resilience4j.retry.instances.recalculateInterest.enable-exponential-backoff=true
|
||||||
|
resilience4j.retry.instances.recalculateInterest.exponential-backoff-multiplier=2
|
||||||
|
resilience4j.retry.instances.recalculateInterest.retryExceptions=org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException
|
||||||
|
|
||||||
|
resilience4j.retry.instances.postInterest.max-attempts=3
|
||||||
|
resilience4j.retry.instances.postInterest.wait-duration=1s
|
||||||
|
resilience4j.retry.instances.postInterest.enable-exponential-backoff=true
|
||||||
|
resilience4j.retry.instances.postInterest.exponential-backoff-multiplier=2
|
||||||
|
resilience4j.retry.instances.postInterest.retryExceptions=org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
#
|
#
|
||||||
|
|
||||||
Feature: Commands Service
|
Feature: Commands Provider
|
||||||
|
|
||||||
@template
|
@template
|
||||||
Scenario Outline: Verify that command providers are injected
|
Scenario Outline: Verify that command providers are injected
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
Feature: Commands Service
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
<logger name="org.springframework.transaction" level="INFO" />
|
<logger name="org.springframework.transaction" level="INFO" />
|
||||||
<logger name="org.springframework.data.convert" level="ERROR" />
|
<logger name="org.springframework.data.convert" level="ERROR" />
|
||||||
<logger name="org.springframework.http.converter.json" level="ERROR" />
|
<logger name="org.springframework.http.converter.json" level="ERROR" />
|
||||||
|
<logger name="io.github.resilience4j.retry" level="DEBUG" />
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="STDOUT" />
|
<appender-ref ref="STDOUT" />
|
||||||
|
|||||||
Reference in New Issue
Block a user