This is an automated email from the ASF dual-hosted git repository.

aleks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 744abd136 FINERACT-1795: Improve resilience of command processing 
service
744abd136 is described below

commit 744abd1364773e7bdeb54a4bcbb4a311b9bea901
Author: Aleks <[email protected]>
AuthorDate: Sun Nov 6 10:47:31 2022 +0100

    FINERACT-1795: Improve resilience of command processing service
---
 .../groovy/org.apache.fineract.dependencies.gradle |   2 +
 .../src/docs/en/chapters/resilience/command.adoc   |  23 +++
 .../src/docs/en/chapters/resilience/index.adoc     |  11 ++
 .../src/docs/en/chapters/resilience/intro.adoc     |  84 ++++++++++
 .../src/docs/en/chapters/resilience/job.adoc       |  25 +++
 .../src/docs/en/chapters/resilience/loan.adoc      |  23 +++
 .../src/docs/en/chapters/resilience/saving.adoc    |  23 +++
 fineract-doc/src/docs/en/index.adoc                |   2 +
 fineract-provider/dependencies.gradle              |   2 +
 ...folioCommandSourceWritePlatformServiceImpl.java |  55 +------
 .../SynchronousCommandProcessingService.java       |  13 +-
 .../domain/FineractPlatformTenantConnection.java   |  19 +--
 ...dularWritePlatformServiceJpaRepositoryImpl.java |   7 +
 .../jobs/service/SchedulerTriggerListener.java     |  48 ++----
 .../security/service/TenantMapper.java             |  20 +--
 .../RecalculateInterestForLoanTasklet.java         |  42 +----
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |  16 +-
 .../service/RecalculateInterestPoster.java         |  50 +-----
 .../PostInterestForSavingTasklet.java              |  26 +---
 ...countWritePlatformServiceJpaRepositoryImpl.java |  95 +++--------
 .../service/SavingsSchedularInterestPoster.java    | 110 +++----------
 .../src/main/resources/application.properties      |  30 +++-
 .../tenant-store/changelog-tenant-store.xml        |   1 +
 .../0006_drop_retry_parameter_columns.xml}         |   7 +-
 .../service/CommandServiceStepDefinitions.java     | 173 +++++++++++++++++++++
 .../src/test/resources/application-test.properties |  23 +++
 .../features/commands/commands.provider.feature    |   2 +-
 ...s.provider.feature => commands.service.feature} |  22 +--
 fineract-provider/src/test/resources/logback.xml   |   1 +
 29 files changed, 541 insertions(+), 414 deletions(-)

diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle 
b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
index 9b74095b5..e77484796 100644
--- a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
+++ b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
@@ -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"
     }
 }
diff --git a/fineract-doc/src/docs/en/chapters/resilience/command.adoc 
b/fineract-doc/src/docs/en/chapters/resilience/command.adoc
new file mode 100644
index 000000000..c1fd0784f
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/resilience/command.adoc
@@ -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]
+----
diff --git a/fineract-doc/src/docs/en/chapters/resilience/index.adoc 
b/fineract-doc/src/docs/en/chapters/resilience/index.adoc
new file mode 100644
index 000000000..200fd7909
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/resilience/index.adoc
@@ -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]
diff --git a/fineract-doc/src/docs/en/chapters/resilience/intro.adoc 
b/fineract-doc/src/docs/en/chapters/resilience/intro.adoc
new file mode 100644
index 000000000..6efa6e850
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/resilience/intro.adoc
@@ -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]
+----
diff --git a/fineract-doc/src/docs/en/chapters/resilience/job.adoc 
b/fineract-doc/src/docs/en/chapters/resilience/job.adoc
new file mode 100644
index 000000000..74c8975eb
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/resilience/job.adoc
@@ -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]
+----
diff --git a/fineract-doc/src/docs/en/chapters/resilience/loan.adoc 
b/fineract-doc/src/docs/en/chapters/resilience/loan.adoc
new file mode 100644
index 000000000..bbb3709d6
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/resilience/loan.adoc
@@ -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]
+----
diff --git a/fineract-doc/src/docs/en/chapters/resilience/saving.adoc 
b/fineract-doc/src/docs/en/chapters/resilience/saving.adoc
new file mode 100644
index 000000000..0b1e7e845
--- /dev/null
+++ b/fineract-doc/src/docs/en/chapters/resilience/saving.adoc
@@ -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]
+----
diff --git a/fineract-doc/src/docs/en/index.adoc 
b/fineract-doc/src/docs/en/index.adoc
index b31430c7d..2c038fe33 100644
--- a/fineract-doc/src/docs/en/index.adoc
+++ b/fineract-doc/src/docs/en/index.adoc
@@ -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]
diff --git a/fineract-provider/dependencies.gradle 
b/fineract-provider/dependencies.gradle
index 07952b14e..e8526710e 100644
--- a/fineract-provider/dependencies.gradle
+++ b/fineract-provider/dependencies.gradle
@@ -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') {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
index c8ebab8c2..188c64c6e 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/commands/service/PortfolioCommandSourceWritePlatformServiceImpl.java
@@ -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
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
 
b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
index 43ae13cb4..a5ea977f3 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/commands/service/SynchronousCommandProcessingService.java
@@ -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;
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java
index 1a5cbaec6..6f2dee679 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/domain/FineractPlatformTenantConnection.java
@@ -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;
     }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedularWritePlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedularWritePlatformServiceJpaRepositoryImpl.java
index b9c81fdc4..f2645cd7c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedularWritePlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedularWritePlatformServiceJpaRepositoryImpl.java
@@ -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;
+    }
+
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerTriggerListener.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerTriggerListener.java
index a48910de9..55b2db779 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerTriggerListener.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerTriggerListener.java
@@ -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);
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TenantMapper.java
 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TenantMapper.java
index 02ade1944..7a4f57a6a 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TenantMapper.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/TenantMapper.java
@@ -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);
     }
 }
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java
index 922d4e986..7d70c478b 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/recalculateinterestforloan/RecalculateInterestForLoanTasklet.java
@@ -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);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 608353ff2..15050010c 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -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) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/RecalculateInterestPoster.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/RecalculateInterestPoster.java
index 1c1cb0e12..6d0846fef 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/RecalculateInterestPoster.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/RecalculateInterestPoster.java
@@ -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);
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
index 88211beaf..209667244 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/jobs/postinterestforsavings/PostInterestForSavingTasklet.java
@@ -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);
 
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
index b3e9d4983..fb9d2d05d 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
@@ -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) {
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
index b2fcc557a..d89814bbf 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsSchedularInterestPoster.java
@@ -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<>();
diff --git a/fineract-provider/src/main/resources/application.properties 
b/fineract-provider/src/main/resources/application.properties
index d04f98c19..bd1a93a11 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -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}
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
index 74dd115ee..402892a9a 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
@@ -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>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0006_drop_retry_parameter_columns.xml
similarity index 80%
copy from 
fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
copy to 
fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0006_drop_retry_parameter_columns.xml
index 74dd115ee..d8376a59f 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant-store/changelog-tenant-store.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant-store/parts/0006_drop_retry_parameter_columns.xml
@@ -22,7 +22,8 @@
 <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";>
-     <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"/>
+    <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>
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java
 
b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java
new file mode 100644
index 000000000..cd8914745
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/commands/service/CommandServiceStepDefinitions.java
@@ -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);
+        }
+    }
+}
diff --git a/fineract-provider/src/test/resources/application-test.properties 
b/fineract-provider/src/test/resources/application-test.properties
index 0247e9519..d19755693 100644
--- a/fineract-provider/src/test/resources/application-test.properties
+++ b/fineract-provider/src/test/resources/application-test.properties
@@ -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
diff --git 
a/fineract-provider/src/test/resources/features/commands/commands.provider.feature
 
b/fineract-provider/src/test/resources/features/commands/commands.provider.feature
index 34fb1518e..b5fea5b20 100644
--- 
a/fineract-provider/src/test/resources/features/commands/commands.provider.feature
+++ 
b/fineract-provider/src/test/resources/features/commands/commands.provider.feature
@@ -17,7 +17,7 @@
 # under the License.
 #
 
-Feature: Commands Service
+Feature: Commands Provider
 
   @template
   Scenario Outline: Verify that command providers are injected
diff --git 
a/fineract-provider/src/test/resources/features/commands/commands.provider.feature
 
b/fineract-provider/src/test/resources/features/commands/commands.service.feature
similarity index 57%
copy from 
fineract-provider/src/test/resources/features/commands/commands.provider.feature
copy to 
fineract-provider/src/test/resources/features/commands/commands.service.feature
index 34fb1518e..be49ab8ab 100644
--- 
a/fineract-provider/src/test/resources/features/commands/commands.provider.feature
+++ 
b/fineract-provider/src/test/resources/features/commands/commands.service.feature
@@ -19,21 +19,9 @@
 
 Feature: Commands Service
 
-  @template
-  Scenario Outline: Verify that command providers are injected
-    Given A command handler for entity <entity> and action <action>
-    When The user processes the command with ID <id>
-    Then The command ID matches <id>
+  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
 
-    Examples:
-      | id  | entity | action |
-      | 815 | HUMAN  | UPDATE |
-
-  @template
-  Scenario Outline: Verify that command no command handler is provided
-    Given A missing command handler for entity <entity> and action <action>
-    Then The system should throw an exception
-
-    Examples:
-      | entity   | action      |
-      | WHATEVER | DOSOMETHING |
diff --git a/fineract-provider/src/test/resources/logback.xml 
b/fineract-provider/src/test/resources/logback.xml
index 30cf96640..9f704a984 100644
--- a/fineract-provider/src/test/resources/logback.xml
+++ b/fineract-provider/src/test/resources/logback.xml
@@ -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" />


Reply via email to