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.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/resilience/index.adoc[leveloffset=+1]
|
||||
|
||||
include::chapters/security/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-security',
|
||||
'org.mapstruct:mapstruct',
|
||||
|
||||
'io.github.resilience4j:resilience4j-spring-boot2',
|
||||
)
|
||||
|
||||
implementation ('org.apache.commons:commons-email') {
|
||||
|
||||
+5
-50
@@ -19,8 +19,6 @@
|
||||
package org.apache.fineract.commands.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import java.security.SecureRandom;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.exception.CommandNotAwaitingApprovalException;
|
||||
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.data.CommandProcessingResult;
|
||||
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.security.service.PlatformSecurityContext;
|
||||
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.transaction.annotation.Transactional;
|
||||
|
||||
@@ -46,8 +40,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Slf4j
|
||||
public class PortfolioCommandSourceWritePlatformServiceImpl implements PortfolioCommandSourceWritePlatformService {
|
||||
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final CommandSourceRepository commandSourceRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
@@ -55,12 +47,10 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
||||
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
||||
|
||||
@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.
|
||||
@@ -76,47 +66,13 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
||||
validateIsUpdateAllowed();
|
||||
|
||||
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);
|
||||
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.getTransactionId(), wrapper.getHref(), wrapper.getProductId(), wrapper.getCreditBureauId(),
|
||||
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
|
||||
@@ -168,9 +124,8 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
||||
return commandSourceInput;
|
||||
}
|
||||
|
||||
private boolean validateIsUpdateAllowed() {
|
||||
return this.schedulerJobRunnerReadService.isUpdatesAllowed();
|
||||
|
||||
private void validateIsUpdateAllowed() {
|
||||
this.schedulerJobRunnerReadService.isUpdatesAllowed();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+12
-1
@@ -20,6 +20,7 @@ package org.apache.fineract.commands.service;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import java.lang.reflect.Type;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
@@ -69,8 +70,9 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
|
||||
private final FineractProperties fineractProperties;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@Transactional
|
||||
@Retry(name = "executeCommand", fallbackMethod = "fallbackExecuteCommand")
|
||||
public CommandProcessingResult executeCommand(final CommandWrapper wrapper, final JsonCommand command,
|
||||
final boolean isApprovedByChecker) {
|
||||
|
||||
@@ -161,6 +163,15 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
.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) {
|
||||
NewCommandSourceHandler handler;
|
||||
|
||||
|
||||
+3
-16
@@ -54,8 +54,6 @@ public class FineractPlatformTenantConnection implements Serializable {
|
||||
private final int suspectTimeout;
|
||||
private final int timeBetweenEvictionRunsMillis;
|
||||
private final int minEvictableIdleTimeMillis;
|
||||
private final int maxRetriesOnDeadlock;
|
||||
private final int maxIntervalBetweenRetries;
|
||||
private final boolean testOnBorrow;
|
||||
|
||||
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 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 timeBetweenEvictionRunsMillis, final int minEvictableIdleTimeMillis, final int maxRetriesOnDeadlock,
|
||||
final int maxIntervalBetweenRetries, final boolean tesOnBorrow, final String readOnlySchemaServer,
|
||||
final String readOnlySchemaServerPort, final String readOnlySchemaName, final String readOnlySchemaUsername,
|
||||
final String readOnlySchemaPassword, final String readOnlySchemaConnectionParameters) {
|
||||
final int timeBetweenEvictionRunsMillis, final int minEvictableIdleTimeMillis, final boolean tesOnBorrow,
|
||||
final String readOnlySchemaServer, final String readOnlySchemaServerPort, final String readOnlySchemaName,
|
||||
final String readOnlySchemaUsername, final String readOnlySchemaPassword, final String readOnlySchemaConnectionParameters) {
|
||||
|
||||
this.connectionId = connectionId;
|
||||
this.schemaName = schemaName;
|
||||
@@ -88,8 +85,6 @@ public class FineractPlatformTenantConnection implements Serializable {
|
||||
this.suspectTimeout = suspectTimeout;
|
||||
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
|
||||
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
|
||||
this.maxRetriesOnDeadlock = maxRetriesOnDeadlock;
|
||||
this.maxIntervalBetweenRetries = maxIntervalBetweenRetries;
|
||||
this.testOnBorrow = tesOnBorrow;
|
||||
this.readOnlySchemaServer = readOnlySchemaServer;
|
||||
this.readOnlySchemaServerPort = readOnlySchemaServerPort;
|
||||
@@ -171,14 +166,6 @@ public class FineractPlatformTenantConnection implements Serializable {
|
||||
return this.minEvictableIdleTimeMillis;
|
||||
}
|
||||
|
||||
public int getMaxRetriesOnDeadlock() {
|
||||
return this.maxRetriesOnDeadlock;
|
||||
}
|
||||
|
||||
public int getMaxIntervalBetweenRetries() {
|
||||
return this.maxIntervalBetweenRetries;
|
||||
}
|
||||
|
||||
public boolean isTestOnBorrow() {
|
||||
return testOnBorrow;
|
||||
}
|
||||
|
||||
+7
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.service;
|
||||
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -133,6 +134,7 @@ public class SchedularWritePlatformServiceJpaRepositoryImpl implements Schedular
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@Retry(name = "processJobDetailForExecution", fallbackMethod = "fallbackProcessJobDetailForExecution")
|
||||
public boolean processJobDetailForExecution(final String jobKey, final String triggerType) {
|
||||
boolean isStopExecution = false;
|
||||
final ScheduledJobDetail scheduledJobDetail = this.scheduledJobDetailsRepository.findByJobKeyWithLock(jobKey);
|
||||
@@ -151,4 +153,9 @@ public class SchedularWritePlatformServiceJpaRepositoryImpl implements Schedular
|
||||
return isStopExecution;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private boolean fallbackProcessJobDetailForExecution(Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-38
@@ -18,11 +18,10 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.jobs.service;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
|
||||
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
||||
@@ -34,19 +33,15 @@ import org.quartz.JobKey;
|
||||
import org.quartz.Trigger;
|
||||
import org.quartz.Trigger.CompletedExecutionInstruction;
|
||||
import org.quartz.TriggerListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
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 TenantDetailsService tenantDetailsService;
|
||||
|
||||
@@ -59,12 +54,10 @@ public class SchedulerTriggerListener implements TriggerListener {
|
||||
|
||||
@Override
|
||||
public void triggerFired(Trigger trigger, JobExecutionContext context) {
|
||||
LOG.debug("triggerFired() trigger={}, context={}", trigger, context);
|
||||
log.debug("triggerFired() trigger={}, context={}", trigger, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressFBWarnings(value = {
|
||||
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
||||
@Transactional(isolation = Isolation.READ_COMMITTED)
|
||||
public boolean vetoJobExecution(final Trigger trigger, final JobExecutionContext context) {
|
||||
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)) {
|
||||
triggerType = context.getMergedJobDataMap().getString(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE);
|
||||
}
|
||||
Integer maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean vetoJob = this.schedularService.processJobDetailForExecution(jobKey, triggerType);
|
||||
if (vetoJob) {
|
||||
LOG.warn(
|
||||
"vetoJobExecution() WILL veto the execution (returning vetoJob == true; the job's execute method will NOT be called); "
|
||||
+ "maxNumberOfRetries={}, tenant={}, jobKey={}, triggerType={}, trigger={}, context={}",
|
||||
maxNumberOfRetries, tenantIdentifier, jobKey, triggerType, trigger, context);
|
||||
log.warn(
|
||||
"vetoJobExecution() WILL veto the execution (returning vetoJob == true; the job's execute method will NOT be called); tenant={}, jobKey={}, triggerType={}, trigger={}, context={}",
|
||||
tenantIdentifier, jobKey, triggerType, trigger, context);
|
||||
}
|
||||
return vetoJob;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void triggerMisfired(final Trigger trigger) {
|
||||
LOG.error("triggerMisfired() trigger={}", trigger);
|
||||
log.error("triggerMisfired() trigger={}", trigger);
|
||||
}
|
||||
|
||||
@Override
|
||||
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_suspect_timeout as poolSuspectTimeout, ts.pool_time_between_eviction_runs_millis as poolTimeBetweenEvictionRunsMillis,"
|
||||
+ " 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_name as readOnlySchemaName, " + " ts.readonly_schema_username as readOnlySchemaUsername, "
|
||||
+ " 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 timeBetweenEvictionRunsMillis = rs.getInt("poolTimeBetweenEvictionRunsMillis");
|
||||
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,
|
||||
schemaUsername, schemaPassword, autoUpdateEnabled, initialSize, validationInterval, removeAbandoned, removeAbandonedTimeout,
|
||||
logAbandoned, abandonWhenPercentageFull, maxActive, minIdle, maxIdle, suspectTimeout, timeBetweenEvictionRunsMillis,
|
||||
minEvictableIdleTimeMillis, maxRetriesOnDeadlock, maxIntervalBetweenRetries, testOnBorrow, readOnlySchemaServer,
|
||||
readOnlySchemaServerPort, readOnlySchemaName, readOnlySchemaUsername, readOnlySchemaPassword,
|
||||
readOnlySchemaConnectionParameters);
|
||||
}
|
||||
|
||||
private int bindValueInMinMaxRange(final int value, int min, int max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
} else if (value > max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
minEvictableIdleTimeMillis, testOnBorrow, readOnlySchemaServer, readOnlySchemaServerPort, readOnlySchemaName,
|
||||
readOnlySchemaUsername, readOnlySchemaPassword, readOnlySchemaConnectionParameters);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-38
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.jobs.recalculateinterestforloan;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -32,7 +31,6 @@ import java.util.concurrent.Future;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.organisation.office.data.OfficeData;
|
||||
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.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.dao.CannotAcquireLockException;
|
||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -56,7 +52,6 @@ public class RecalculateInterestForLoanTasklet implements Tasklet {
|
||||
private final LoanWritePlatformService loanWritePlatformService;
|
||||
private final RecalculateInterestPoster recalculateInterestPoster;
|
||||
private final OfficeReadPlatformService officeReadPlatformService;
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||
@@ -75,45 +70,16 @@ public class RecalculateInterestForLoanTasklet implements Tasklet {
|
||||
|
||||
recalculateInterest(office, threadPoolSize, batchSize);
|
||||
} else {
|
||||
int maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
||||
int maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
|
||||
Collection<Long> loanIds = loanReadPlatformService.fetchLoansForInterestRecalculation();
|
||||
int i = 0;
|
||||
if (!loanIds.isEmpty()) {
|
||||
List<Throwable> errors = new ArrayList<>();
|
||||
for (Long loanId : loanIds) {
|
||||
log.debug("recalculateInterest: Loan ID = {}", loanId);
|
||||
int numberOfRetries = 0;
|
||||
while (numberOfRetries <= maxNumberOfRetries) {
|
||||
try {
|
||||
loanWritePlatformService.recalculateInterest(loanId);
|
||||
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++;
|
||||
try {
|
||||
loanWritePlatformService.recalculateInterest(loanId);
|
||||
} catch (Exception e) {
|
||||
errors.add(e);
|
||||
}
|
||||
log.debug("recalculateInterest: Loans count {}", i);
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
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.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -39,7 +40,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.service.LoanAccountLockService;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
@@ -281,7 +281,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
private final PostDatedChecksRepository postDatedChecksRepository;
|
||||
private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository;
|
||||
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
|
||||
private final LoanAccountLockRepository loanAccountLockRepository;
|
||||
private final LoanAccountLockService loanAccountLockService;
|
||||
|
||||
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
|
||||
@@ -3217,6 +3216,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@Retry(name = "recalculateInterest", fallbackMethod = "fallbackRecalculateInterest")
|
||||
public void recalculateInterest(final long loanId) {
|
||||
Loan loan = this.loanAssembler.assembleFrom(loanId);
|
||||
LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate();
|
||||
@@ -3252,6 +3252,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
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) {
|
||||
final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId);
|
||||
if (transferTransaction != null) {
|
||||
|
||||
+4
-46
@@ -18,19 +18,14 @@
|
||||
*/
|
||||
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.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.dao.CannotAcquireLockException;
|
||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -38,7 +33,6 @@ import org.springframework.stereotype.Component;
|
||||
public class RecalculateInterestPoster implements Callable<Void> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RecalculateInterestPoster.class);
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
private Collection<Long> loanIds;
|
||||
private LoanWritePlatformService loanWritePlatformService;
|
||||
@@ -52,52 +46,16 @@ public class RecalculateInterestPoster implements Callable<Void> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressFBWarnings(value = {
|
||||
"DMI_RANDOM_USED_ONLY_ONCE" }, justification = "False positive for random object created and used only once")
|
||||
public Void call() throws JobExecutionException {
|
||||
Integer maxNumberOfRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxRetriesOnDeadlock();
|
||||
Integer maxIntervalBetweenRetries = ThreadLocalContextUtil.getTenant().getConnection().getMaxIntervalBetweenRetries();
|
||||
|
||||
int i = 0;
|
||||
if (!loanIds.isEmpty()) {
|
||||
List<Throwable> errors = new ArrayList<>();
|
||||
for (Long loanId : loanIds) {
|
||||
LOG.debug("Loan ID {}", loanId);
|
||||
Integer numberOfRetries = 0;
|
||||
while (numberOfRetries <= maxNumberOfRetries) {
|
||||
try {
|
||||
this.loanWritePlatformService.recalculateInterest(loanId);
|
||||
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++;
|
||||
try {
|
||||
loanWritePlatformService.recalculateInterest(loanId);
|
||||
} catch (Exception e) {
|
||||
errors.add(e);
|
||||
}
|
||||
LOG.debug("Loans count {}", i);
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
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.core.domain.FineractContext;
|
||||
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.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.SavingsAccountWritePlatformService;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsSchedularInterestPoster;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
import org.springframework.batch.core.scope.context.ChunkContext;
|
||||
import org.springframework.batch.core.step.tasklet.Tasklet;
|
||||
import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@@ -59,15 +53,9 @@ public class PostInterestForSavingTasklet implements Tasklet {
|
||||
|
||||
private final SavingsAccountReadPlatformService savingAccountReadPlatformService;
|
||||
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 ApplicationContext applicationContext;
|
||||
private final int queueSize = 1;
|
||||
private final PlatformSecurityContext securityContext;
|
||||
|
||||
@Override
|
||||
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++) {
|
||||
List<SavingsAccountData> subList = safeSubList(savingsAccounts, fromIndex, toIndex);
|
||||
SavingsSchedularInterestPoster savingsSchedularInterestPoster = (SavingsSchedularInterestPoster) applicationContext
|
||||
.getBean("savingsSchedularInterestPoster");
|
||||
SavingsSchedularInterestPoster savingsSchedularInterestPoster = applicationContext
|
||||
.getBean(SavingsSchedularInterestPoster.class);
|
||||
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.setTransactionTemplate(transactionTemplate);
|
||||
savingsSchedularInterestPoster.setConfigurationDomainService(configurationDomainService);
|
||||
savingsSchedularInterestPoster.setPlatformSecurityContext(securityContext);
|
||||
savingsSchedularInterestPoster.setContext(ThreadLocalContextUtil.getContext());
|
||||
|
||||
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.JsonElement;
|
||||
import io.github.resilience4j.retry.annotation.Retry;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -46,6 +47,8 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||
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.security.service.PlatformSecurityContext;
|
||||
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.MoneyHelper;
|
||||
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.useradministration.domain.AppUser;
|
||||
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.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements SavingsAccountWritePlatformService {
|
||||
|
||||
@@ -149,7 +149,6 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
private final SavingsAccountTransactionDataValidator savingsAccountTransactionDataValidator;
|
||||
private final SavingsAccountChargeDataValidator savingsAccountChargeDataValidator;
|
||||
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
|
||||
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
|
||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||
private final SavingsAccountDomainService savingsAccountDomainService;
|
||||
private final NoteRepository noteRepository;
|
||||
@@ -166,63 +165,8 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
private final StandingInstructionRepository standingInstructionRepository;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final GSIMRepositoy gsimRepository;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
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
|
||||
@Override
|
||||
public CommandProcessingResult gsimActivate(final Long gsimId, final JsonCommand command) {
|
||||
@@ -355,7 +299,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
|
||||
if (account.getGsim() != null) {
|
||||
isGsim = true;
|
||||
LOG.debug("is gsim");
|
||||
log.debug("is gsim");
|
||||
}
|
||||
checkClientOrGroupActive(account);
|
||||
|
||||
@@ -376,16 +320,16 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
|
||||
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();
|
||||
LOG.debug("parent deposit : {} ", gsim.getParentDeposit());
|
||||
LOG.debug("child account : {} ", savingsId);
|
||||
log.debug("parent deposit : {} ", gsim.getParentDeposit());
|
||||
log.debug("child account : {} ", savingsId);
|
||||
BigDecimal currentBalance = gsim.getParentDeposit();
|
||||
BigDecimal newBalance = currentBalance.add(transactionAmount);
|
||||
gsim.setParentDeposit(newBalance);
|
||||
gsimRepository.save(gsim);
|
||||
LOG.debug("balance after making deposit : {} ",
|
||||
log.debug("balance after making deposit : {} ",
|
||||
gsimRepository.findById(account.getGsim().getId()).orElseThrow().getParentDeposit());
|
||||
|
||||
}
|
||||
@@ -641,6 +585,7 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@Retry(name = "postInterest", fallbackMethod = "fallbackPostInterest")
|
||||
public SavingsAccountData postInterest(SavingsAccountData savingsAccountData, final boolean postInterestAs,
|
||||
final LocalDate transactionDate, final boolean backdatedTxnsAllowedTill) {
|
||||
|
||||
@@ -662,14 +607,9 @@ public class SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
|
||||
postInterestOnDate = transactionDate;
|
||||
}
|
||||
|
||||
long startPosting = System.currentTimeMillis();
|
||||
LOG.debug("Interest Posting Start Here at {}", startPosting);
|
||||
|
||||
savingsAccountData = this.savingsAccountInterestPostingService.postInterest(mc, today, isInterestTransfer,
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
|
||||
savingsAccountData);
|
||||
long endPosting = System.currentTimeMillis();
|
||||
LOG.debug("Interest Posting Ends within {}", endPosting - startPosting);
|
||||
|
||||
if (!backdatedTxnsAllowedTill) {
|
||||
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
|
||||
private SavingsAccountTransaction payCharge(final SavingsAccountCharge savingsAccountCharge, final LocalDate transactionDate,
|
||||
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;
|
||||
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import java.math.BigDecimal;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
@@ -30,11 +28,10 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.service.DateUtils;
|
||||
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.SavingsAccountSummaryData;
|
||||
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.dao.CannotAcquireLockException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.orm.ObjectOptimisticLockingFailureException;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
/**
|
||||
* @author manoj
|
||||
*/
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Setter
|
||||
@Component
|
||||
@Scope("prototype")
|
||||
@Setter
|
||||
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 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 SavingsAccountWritePlatformService savingsAccountWritePlatformService;
|
||||
private SavingsAccountRepositoryWrapper savingsAccountRepository;
|
||||
private SavingsAccountAssembler savingAccountAssembler;
|
||||
private FineractContext context;
|
||||
private ConfigurationDomainService configurationDomainService;
|
||||
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
|
||||
@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)
|
||||
public Void call() throws org.apache.fineract.infrastructure.jobs.exception.JobExecutionException {
|
||||
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()) {
|
||||
List<Throwable> errors = new ArrayList<>();
|
||||
for (SavingsAccountData savingsAccountData : savingAccounts) {
|
||||
Integer numberOfRetries = 0;
|
||||
while (numberOfRetries <= maxNumberOfRetries) {
|
||||
try {
|
||||
boolean postInterestAsOn = false;
|
||||
LocalDate transactionDate = null;
|
||||
long startPosting = System.currentTimeMillis();
|
||||
SavingsAccountData savingsAccountDataRet = savingsAccountWritePlatformService.postInterest(savingsAccountData,
|
||||
postInterestAsOn, transactionDate, backdatedTxnsAllowedTill);
|
||||
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);
|
||||
}
|
||||
boolean postInterestAsOn = false;
|
||||
LocalDate transactionDate = null;
|
||||
try {
|
||||
SavingsAccountData savingsAccountDataRet = savingsAccountWritePlatformService.postInterest(savingsAccountData,
|
||||
postInterestAsOn, transactionDate, backdatedTxnsAllowedTill);
|
||||
savingsAccountDataList.add(savingsAccountDataRet);
|
||||
} catch (Exception e) {
|
||||
errors.add(e);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
try {
|
||||
batchUpdate(savingsAccountDataList);
|
||||
} catch (DataAccessException exception) {
|
||||
LOG.error("Batch update failed due to DataAccessException", exception);
|
||||
log.error("Batch update failed due to DataAccessException", exception);
|
||||
errors.add(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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -297,10 +233,10 @@ public class SavingsSchedularInterestPoster implements Callable<Void> {
|
||||
this.jdbcTemplate.batchUpdate(queryForSavingsUpdate, paramsForSavingsSummary);
|
||||
this.jdbcTemplate.batchUpdate(queryForTransactionInsertion, paramsForTransactionInsertion);
|
||||
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);
|
||||
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<>();
|
||||
|
||||
@@ -73,17 +73,18 @@ fineract.loan.transactionprocessor.error-not-found-fail=${FINERACT_LOAN_TRANSACT
|
||||
# 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}}
|
||||
|
||||
management.health.jms.enabled=false
|
||||
management.health.jms.enabled=${FINERACT_MANAGEMENT_HEALTH_JMS_ENABLED:false}
|
||||
|
||||
# FINERACT 1296
|
||||
management.endpoint.health.probes.enabled=true
|
||||
management.health.livenessState.enabled=true
|
||||
management.health.readinessState.enabled=true
|
||||
|
||||
management.health.ratelimiters.enabled=${FINERACT_MANAGEMENT_HEALTH_RATELIMITERS_ENABLED:false}
|
||||
|
||||
# FINERACT-883
|
||||
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
|
||||
server.forward-headers-strategy=framework
|
||||
server.port=${FINERACT_SERVER_PORT:8443}
|
||||
@@ -164,3 +165,26 @@ spring.main.allow-bean-definition-overriding=true
|
||||
spring.batch.initialize-schema=NEVER
|
||||
# Disabling Spring Batch jobs on startup
|
||||
spring.batch.job.enabled=false
|
||||
|
||||
resilience4j.retry.instances.executeCommand.max-attempts=${FINERACT_COMMAND_PROCESSING_RETRY_MAX_ATTEMPTS:3}
|
||||
resilience4j.retry.instances.executeCommand.wait-duration=${FINERACT_COMMAND_PROCESSING_RETRY_WAIT_DURATION:1s}
|
||||
resilience4j.retry.instances.executeCommand.enable-exponential-backoff=${FINERACT_COMMAND_PROCESSING_RETRY_ENABLE_EXPONENTIAL_BACKOFF:true}
|
||||
resilience4j.retry.instances.executeCommand.exponential-backoff-multiplier=${FINERACT_COMMAND_PROCESSING_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER:2}
|
||||
resilience4j.retry.instances.executeCommand.retryExceptions=${FINERACT_COMMAND_PROCESSING_RETRY_EXCEPTIONS:org.springframework.dao.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/0004_readonly_database_connection.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>
|
||||
|
||||
+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
|
||||
# Disabling Spring Batch jobs on startup
|
||||
spring.batch.job.enabled=false
|
||||
|
||||
resilience4j.retry.instances.executeCommand.max-attempts=3
|
||||
resilience4j.retry.instances.executeCommand.wait-duration=1s
|
||||
resilience4j.retry.instances.executeCommand.enable-exponential-backoff=true
|
||||
resilience4j.retry.instances.executeCommand.exponential-backoff-multiplier=2
|
||||
resilience4j.retry.instances.executeCommand.retryExceptions=org.springframework.dao.CannotAcquireLockException,org.springframework.orm.ObjectOptimisticLockingFailureException
|
||||
|
||||
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.
|
||||
#
|
||||
|
||||
Feature: Commands Service
|
||||
Feature: Commands Provider
|
||||
|
||||
@template
|
||||
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.data.convert" level="ERROR" />
|
||||
<logger name="org.springframework.http.converter.json" level="ERROR" />
|
||||
<logger name="io.github.resilience4j.retry" level="DEBUG" />
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="STDOUT" />
|
||||
|
||||
Reference in New Issue
Block a user