This is an automated email from the ASF dual-hosted git repository.
adamsaghy 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 6d3e19cf25 FINERACT-2234: add extra caches to frequent database
queries during the interest recalculation
6d3e19cf25 is described below
commit 6d3e19cf25967cdc6e98d2e6705af80c00a968ab
Author: budaidev <[email protected]>
AuthorDate: Tue Apr 22 21:50:35 2025 +0200
FINERACT-2234: add extra caches to frequent database queries during the
interest recalculation
---
.../domain/ApplicationCurrencyRepository.java | 48 ++++
.../domain/CalendarInstanceRepository.java | 48 +++-
.../service/LoanScheduleAssembler.java | 2 +-
.../serialization/LoanApplicationValidator.java | 2 +-
.../serialization/LoanTransactionValidator.java | 2 +-
.../loanaccount/service/LoanUtilService.java | 10 +-
.../LoanCOBPerformanceRestTest.java | 280 +++++++++++++++++++++
7 files changed, 382 insertions(+), 10 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepository.java
b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepository.java
index 4c50c37d71..0128bd5156 100644
---
a/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepository.java
+++
b/fineract-core/src/main/java/org/apache/fineract/organisation/monetary/domain/ApplicationCurrencyRepository.java
@@ -20,6 +20,9 @@ package org.apache.fineract.organisation.monetary.domain;
import java.util.List;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
+import org.springframework.cache.annotation.CacheConfig;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
@@ -28,16 +31,61 @@ import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
+@CacheConfig(cacheNames = "currencies")
public interface ApplicationCurrencyRepository
extends JpaRepository<ApplicationCurrency, Long>,
JpaSpecificationExecutor<ApplicationCurrency> {
String FIND_CURRENCY_DETAILS = "SELECT new
org.apache.fineract.organisation.monetary.data.CurrencyData(ac.code, ac.name,
ac.decimalPlaces, ac.inMultiplesOf, ac.displaySymbol, ac.nameCode) FROM
ApplicationCurrency ac ";
+ @Cacheable(key = "'entity_' + #currencyCode")
ApplicationCurrency findOneByCode(String currencyCode);
+ @Cacheable(key = "'data_' + #currencyCode")
@Query(FIND_CURRENCY_DETAILS + " WHERE ac.code = :code")
CurrencyData findCurrencyDataByCode(@Param("code") String currencyCode);
+ @Cacheable
@Query(FIND_CURRENCY_DETAILS)
List<CurrencyData> findAllSorted(Sort sort);
+
+ /**
+ * Override save method with cache eviction
+ */
+ @Override
+ @CacheEvict(allEntries = true)
+ <S extends ApplicationCurrency> S save(S entity);
+
+ /**
+ * Override saveAll method with cache eviction
+ */
+ @Override
+ @CacheEvict(allEntries = true)
+ <S extends ApplicationCurrency> List<S> saveAll(Iterable<S> entities);
+
+ /**
+ * Override delete methods with cache eviction
+ */
+ @Override
+ @CacheEvict(allEntries = true)
+ void delete(ApplicationCurrency entity);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteAll();
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteAll(Iterable<? extends ApplicationCurrency> entities);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteById(Long id);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteAllById(Iterable<? extends Long> ids);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ <S extends ApplicationCurrency> S saveAndFlush(S entity);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/domain/CalendarInstanceRepository.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/domain/CalendarInstanceRepository.java
index 2ec0fae95a..1250c7509b 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/domain/CalendarInstanceRepository.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/calendar/domain/CalendarInstanceRepository.java
@@ -25,15 +25,23 @@ import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
+import org.springframework.cache.annotation.CacheConfig;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+@Repository
+@CacheConfig(cacheNames = "calendarInstances")
public interface CalendarInstanceRepository extends
JpaRepository<CalendarInstance, Long>,
JpaSpecificationExecutor<CalendarInstance> {
+ @Cacheable(key = "'calId_' + #calendarId + '_entityId_' + #entityId +
'_entityTypeId_' + #entityTypeId")
CalendarInstance findByCalendarIdAndEntityIdAndEntityTypeId(Long
calendarId, Long entityId, Integer entityTypeId);
+ @Cacheable(key = "'entityId_' + #entityId + '_entityTypeId_' +
#entityTypeId")
Collection<CalendarInstance> findByEntityIdAndEntityTypeId(Long entityId,
Integer entityTypeId);
/**
@@ -45,14 +53,18 @@ public interface CalendarInstanceRepository extends
JpaRepository<CalendarInstan
* {@link CalendarType}
* @return
*/
+ @Cacheable(key = "'entityId_' + #entityId + '_entityTypeId_' +
#entityTypeId + '_calendarTypeId_' + #calendarTypeId")
CalendarInstance findByEntityIdAndEntityTypeIdAndCalendarTypeId(Long
entityId, Integer entityTypeId, Integer calendarTypeId);
+ @Cacheable(key = "'findCalendarInstanceByEntityId_entityId_' + #entityId +
'_entityTypeId_' + #entityTypeId")
@Query("select ci from CalendarInstance ci where ci.entityId = :entityId
and ci.entityTypeId = :entityTypeId")
- CalendarInstance findCalendarInstaneByEntityId(@Param("entityId") Long
entityId, @Param("entityTypeId") Integer entityTypeId);
+ CalendarInstance findCalendarInstanceByEntityId(@Param("entityId") Long
entityId, @Param("entityTypeId") Integer entityTypeId);
+ @Cacheable(key = "'calendarId_' + #calendarId + '_entityTypeId_' +
#entityTypeId")
Collection<CalendarInstance> findByCalendarIdAndEntityTypeId(Long
calendarId, Integer entityTypeId);
/** Should use in clause, can I do it without creating a new class? **/
+ @Cacheable(key = "'groupId_' + #groupId + '_clientId_' + #clientId +
'_statuses_' +
T(org.springframework.util.StringUtils).collectionToCommaDelimitedString(#loanStatuses)")
@Query("select ci from CalendarInstance ci where ci.entityId in (select
loan.id from Loan loan where loan.client.id = :clientId and loan.group.id =
:groupId and loan.loanStatus in :loanStatuses) and ci.entityTypeId = 3")
List<CalendarInstance>
findCalendarInstancesForLoansByGroupIdAndClientIdAndStatuses(@Param("groupId")
Long groupId,
@Param("clientId") Long clientId, @Param("loanStatuses")
Collection<LoanStatus> loanStatuses);
@@ -60,9 +72,41 @@ public interface CalendarInstanceRepository extends
JpaRepository<CalendarInstan
/**
* EntityType = 3 is for loan
*/
-
+ @Cacheable(key = "'countLoans_calendarId_' + #calendarId + '_statuses_' +
T(org.springframework.util.StringUtils).collectionToCommaDelimitedString(#loanStatuses)")
@Query("SELECT COUNT(ci.id) FROM CalendarInstance ci, Loan loan WHERE
loan.id = ci.entityId AND ci.entityTypeId = 3 AND ci.calendar.id = :calendarId
AND loan.loanStatus IN :loanStatuses ")
Integer countOfLoansSyncedWithCalendar(@Param("calendarId") Long
calendarId,
@Param("loanStatuses") Collection<LoanStatus> loanStatuses);
+ // Override JpaRepository methods to add cache eviction
+ @Override
+ @CacheEvict(allEntries = true)
+ <S extends CalendarInstance> S save(S entity);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ <S extends CalendarInstance> List<S> saveAll(Iterable<S> entities);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void delete(CalendarInstance entity);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteById(Long id);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteAll();
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteAll(Iterable<? extends CalendarInstance> entities);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ void deleteAllById(Iterable<? extends Long> ids);
+
+ @Override
+ @CacheEvict(allEntries = true)
+ <S extends CalendarInstance> S saveAndFlush(S entity);
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
index 2db86adb9b..07db84d813 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java
@@ -916,7 +916,7 @@ public class LoanScheduleAssembler {
overlappings);
}
LoanProductVariableInstallmentConfig installmentConfig =
loan.loanProduct().loanProductVariableInstallmentConfig();
- final CalendarInstance loanCalendarInstance =
calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
+ final CalendarInstance loanCalendarInstance =
calendarInstanceRepository.findCalendarInstanceByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
Calendar loanCalendar = null;
if (loanCalendarInstance != null) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java
index 9e74e10db0..5ef4309827 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java
@@ -2164,7 +2164,7 @@ public final class LoanApplicationValidator {
}
private Calendar getCalendarInstance(Loan loan) {
- CalendarInstance calendarInstance =
calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
+ CalendarInstance calendarInstance =
calendarInstanceRepository.findCalendarInstanceByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
return calendarInstance != null ? calendarInstance.getCalendar() :
null;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java
index ac65ac45f5..80c7a7a47a 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java
@@ -195,7 +195,7 @@ public final class LoanTransactionValidator {
StatusEnum.DISBURSE.getValue(),
EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
ScheduleGeneratorDTO scheduleGeneratorDTO =
this.loanUtilService.buildScheduleGeneratorDTO(loan, null);
- final CalendarInstance calendarInstance =
this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
+ final CalendarInstance calendarInstance =
this.calendarInstanceRepository.findCalendarInstanceByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
if (loan.isSyncDisbursementWithMeeting()) {
validateDisbursementDateWithMeetingDate(actualDisbursementDate,
calendarInstance,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java
index 7a0c5f9535..6f25497d23 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanUtilService.java
@@ -78,7 +78,7 @@ public class LoanUtilService {
}
final MonetaryCurrency currency = loan.getCurrency();
ApplicationCurrency applicationCurrency =
this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency);
- final CalendarInstance calendarInstance =
this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
+ final CalendarInstance calendarInstance =
this.calendarInstanceRepository.findCalendarInstanceByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
Calendar calendar = null;
CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
@@ -93,10 +93,10 @@ public class LoanUtilService {
CalendarInstance compoundingCalendarInstance = null;
Long overdurPenaltyWaitPeriod = null;
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
- restCalendarInstance =
calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
+ restCalendarInstance =
calendarInstanceRepository.findCalendarInstanceByEntityId(loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue());
- compoundingCalendarInstance =
calendarInstanceRepository.findCalendarInstaneByEntityId(loan.loanInterestRecalculationDetailId(),
-
CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue());
+ compoundingCalendarInstance =
calendarInstanceRepository.findCalendarInstanceByEntityId(
+ loan.loanInterestRecalculationDetailId(),
CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue());
overdurPenaltyWaitPeriod =
this.configurationDomainService.retrievePenaltyWaitPeriod();
}
final Boolean isInterestChargedFromDateAsDisbursementDateEnabled =
this.configurationDomainService
@@ -156,7 +156,7 @@ public class LoanUtilService {
}
public LocalDate getCalculatedRepaymentsStartingFromDate(final Loan loan) {
- final CalendarInstance calendarInstance =
this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
+ final CalendarInstance calendarInstance =
this.calendarInstanceRepository.findCalendarInstanceByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null;
return
this.getCalculatedRepaymentsStartingFromDate(loan.getDisbursementDate(), loan,
calendarInstance, calendarHistoryDataWrapper);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBPerformanceRestTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBPerformanceRestTest.java
new file mode 100644
index 0000000000..919e9f0f8f
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBPerformanceRestTest.java
@@ -0,0 +1,280 @@
+/**
+ * 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.integrationtests;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.integrationtests.common.BusinessStepHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/***
+ * This test class is designed to measure the performance of the Loan Close of
Business (COB) process in Fineract. It
+ * creates a specified number of loans, runs the COB process, and measures the
time taken for each step. The results are
+ * printed in a consolidated report at the end of all tests.
+ ***/
+@Disabled("This test is disabled by default. To run it, please remove the
@Disabled annotation.")
+@TestInstance(Lifecycle.PER_CLASS)
+public class LoanCOBPerformanceRestTest extends BaseLoanIntegrationTest {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(LoanCOBPerformanceRestTest.class);
+
+ private static ResponseSpecification responseSpec;
+ private static RequestSpecification requestSpec;
+ private static PostClientsResponse client;
+ private static InlineLoanCOBHelper inlineLoanCOBHelper;
+ private static BusinessStepHelper businessStepHelper;
+ private Random random = new Random();
+
+ // Store metrics for all test runs
+ private Map<String, Map<String, Object>> allTestMetrics = new
LinkedHashMap<>();
+
+ @BeforeAll
+ public static void setup() {
+ Utils.initializeRESTAssured();
+ requestSpec = new
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ requestSpec.header("Authorization", "Basic " +
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+ requestSpec.header("Fineract-Platform-TenantId", Utils.DEFAULT_TENANT);
+ responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+ ClientHelper clientHelper = new ClientHelper(requestSpec,
responseSpec);
+ client =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+ inlineLoanCOBHelper = new InlineLoanCOBHelper(requestSpec,
responseSpec);
+ businessStepHelper = new BusinessStepHelper();
+ // setup COB Business Steps to prevent test failing due other
integration test configurations
+ businessStepHelper.updateSteps("LOAN_CLOSE_OF_BUSINESS",
"APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION",
+ "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE",
"UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+ "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS",
"ACCRUAL_ACTIVITY_POSTING", "LOAN_INTEREST_RECALCULATION");
+ }
+
+ @AfterAll
+ public void printConsolidatedReport() {
+ LOG.info("\n\n");
+
LOG.info("========================================================================");
+ LOG.info(" CONSOLIDATED PERFORMANCE REPORT
");
+
LOG.info("========================================================================");
+
+ // Table header
+ LOG.info(String.format("%-25s | %-20s | %-20s | %-20s", "Test
Configuration", "Loan Creation Time", "First COB Run Time",
+ "Second COB Run Time"));
+
LOG.info("-------------------------------------------------------------------------");
+
+ // Table rows
+ for (Map.Entry<String, Map<String, Object>> entry :
allTestMetrics.entrySet()) {
+ String testName = entry.getKey();
+ Map<String, Object> metrics = entry.getValue();
+
+ int loanCount = (int) metrics.get("loanCount");
+ long createTime = (long) metrics.get("loanCreationTimeMs");
+ long firstCobTime = (long) metrics.get("firstCOBTimeMs");
+ long secondCobTime = (long) metrics.get("secondCOBTimeMs");
+
+ LOG.info(String.format("%-25s | %-20s | %-20s | %-20s", testName +
" (" + loanCount + " loans)",
+ createTime + " ms (" + (createTime / loanCount) + "
ms/loan)",
+ firstCobTime + " ms (" + (firstCobTime / loanCount) + "
ms/loan)",
+ secondCobTime + " ms (" + (secondCobTime / loanCount) + "
ms/loan)"));
+ }
+
+
LOG.info("========================================================================");
+
+ // Add scaled performance analysis if there are multiple tests
+ if (allTestMetrics.size() > 1) {
+ LOG.info("\n");
+ LOG.info("SCALING ANALYSIS");
+
LOG.info("========================================================================");
+ LOG.info("This analysis shows how performance scales with
increasing loan counts");
+
+ // Find the smallest and largest loan count tests
+ int minLoans = Integer.MAX_VALUE;
+ int maxLoans = 0;
+ String minTestName = "";
+ String maxTestName = "";
+
+ for (Map.Entry<String, Map<String, Object>> entry :
allTestMetrics.entrySet()) {
+ int loanCount = (int) entry.getValue().get("loanCount");
+ if (loanCount < minLoans) {
+ minLoans = loanCount;
+ minTestName = entry.getKey();
+ }
+ if (loanCount > maxLoans) {
+ maxLoans = loanCount;
+ maxTestName = entry.getKey();
+ }
+ }
+
+ if (!minTestName.equals(maxTestName)) {
+ Map<String, Object> minMetrics =
allTestMetrics.get(minTestName);
+ Map<String, Object> maxMetrics =
allTestMetrics.get(maxTestName);
+
+ double loanCountRatio = (double) maxLoans / minLoans;
+
+ double createTimeRatio = (double) ((long)
maxMetrics.get("loanCreationTimeMs"))
+ / ((long) minMetrics.get("loanCreationTimeMs"));
+
+ double firstCOBRatio = (double) ((long)
maxMetrics.get("firstCOBTimeMs")) / ((long) minMetrics.get("firstCOBTimeMs"));
+
+ double secondCOBRatio = (double) ((long)
maxMetrics.get("secondCOBTimeMs")) / ((long) minMetrics.get("secondCOBTimeMs"));
+
+ LOG.info(String.format("Loan count increased by a factor of
%.2f (from %d to %d)", loanCountRatio, minLoans, maxLoans));
+ LOG.info(String.format("Loan creation time increased by a
factor of %.2f (scaling efficiency: %.2f%%)", createTimeRatio,
+ (loanCountRatio / createTimeRatio) * 100));
+ LOG.info(String.format("First COB run time increased by a
factor of %.2f (scaling efficiency: %.2f%%)", firstCOBRatio,
+ (loanCountRatio / firstCOBRatio) * 100));
+ LOG.info(String.format("Second COB run time increased by a
factor of %.2f (scaling efficiency: %.2f%%)", secondCOBRatio,
+ (loanCountRatio / secondCOBRatio) * 100));
+
LOG.info("------------------------------------------------------------------------");
+ LOG.info("Note: Scaling efficiency > 100% indicates better
than linear scaling");
+ LOG.info(" Scaling efficiency < 100% indicates worse than
linear scaling");
+ }
+
+
LOG.info("========================================================================");
+ }
+ }
+
+ private Long createLoanProduct(String disbursementDate, Double amount,
Double interestRate, Integer numberOfInstallments) {
+ PostLoanProductsResponse loanProduct = loanProductHelper
+
.createLoanProduct(create4IProgressive().recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY));
+
+ Long loanId = applyAndApproveProgressiveLoan(client.getClientId(),
loanProduct.getResourceId(), disbursementDate, amount,
+ interestRate, numberOfInstallments, null);
+ disburseLoan(loanId, BigDecimal.valueOf(amount), disbursementDate);
+ return loanId;
+ }
+
+ public List<Long> createLoans(int numberOfLoans, String disbursementDate,
Double amount, Double interestRate,
+ Integer numberOfInstallments) {
+ LOG.info("Creating {} loans...", numberOfLoans);
+ long startTime = System.nanoTime();
+
+ List<Long> loanIds = new ArrayList<>();
+ for (int i = 0; i < numberOfLoans; i++) {
+ Long loanId = createLoanProduct(disbursementDate, amount != null ?
amount : getRandomAmount(),
+ interestRate != null ? interestRate :
getRandomInterestRate(),
+ numberOfInstallments != null ? numberOfInstallments :
getRandomNumberOfInstallments());
+ loanIds.add(loanId);
+
+ if ((i + 1) % 10 == 0) {
+ LOG.info("Created {} loans so far...", (i + 1));
+ }
+ }
+
+ long endTime = System.nanoTime();
+ long durationMs = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
+ LOG.info("Loan creation completed in {} ms ({} ms per loan)",
durationMs, durationMs / numberOfLoans);
+
+ return loanIds;
+ }
+
+ // random number from 3,4,6,12
+ private Integer getRandomNumberOfInstallments() {
+ int[] possibleValues = { 3, 4, 6, 12 };
+ return possibleValues[random.nextInt(possibleValues.length)];
+ }
+
+ private Double getRandomAmount() {
+ return random.nextInt(1, 100) * 100.0;
+ }
+
+ private Double getRandomInterestRate() {
+ return (double) random.nextInt(1, 15);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = { 10, 50, 100 })
+ public void testLoanCOBPerformanceWithDifferentLoansCount(int loanCount,
TestInfo testInfo) {
+ String testName = testInfo.getDisplayName();
+ LOG.info("Starting test: {} with {} loans", testName, loanCount);
+
+ AtomicReference<List<Long>> loanIds = new AtomicReference<>(new
ArrayList<>());
+ final Map<String, Object> metrics = new HashMap<>();
+ metrics.put("loanCount", loanCount);
+
+ // Create loans
+ runAt("1 January 2023", () -> {
+ long startTime = System.nanoTime();
+ loanIds.set(createLoans(loanCount, "1 January 2023", null, null,
null));
+ long endTime = System.nanoTime();
+ long duration = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
+ metrics.put("loanCreationTimeMs", duration);
+ LOG.info("Created {} loans in {} ms", loanCount, duration);
+ });
+
+ // First COB run - January to February
+ runAt("1 February 2023", () -> {
+ LOG.info("Running first COB for {} loans...", loanCount);
+ long startTime = System.nanoTime();
+ inlineLoanCOBHelper.executeInlineCOB(loanIds.get());
+ long endTime = System.nanoTime();
+ long duration = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
+ metrics.put("firstCOBTimeMs", duration);
+ LOG.info("First COB completed in {} ms ({} ms per loan)",
duration, duration / loanCount);
+ });
+
+ // Second COB run - February to March
+ runAt("1 March 2023", () -> {
+ LOG.info("Running second COB for {} loans...", loanCount);
+ long startTime = System.nanoTime();
+ inlineLoanCOBHelper.executeInlineCOB(loanIds.get());
+ long endTime = System.nanoTime();
+ long duration = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);
+ metrics.put("secondCOBTimeMs", duration);
+ LOG.info("Second COB completed in {} ms ({} ms per loan)",
duration, duration / loanCount);
+ });
+
+ // Add metrics to the consolidated collection
+ allTestMetrics.put(testName, metrics);
+
+ // Print individual test summary
+ LOG.info("Individual test complete. Summary for {} loans:", loanCount);
+ LOG.info("----------------------------------------------------");
+ LOG.info("Loan Creation Time: {} ms ({} ms/loan)",
metrics.get("loanCreationTimeMs"),
+ ((Long) metrics.get("loanCreationTimeMs")) / loanCount);
+ LOG.info("First COB Run Time: {} ms ({} ms/loan)",
metrics.get("firstCOBTimeMs"),
+ ((Long) metrics.get("firstCOBTimeMs")) / loanCount);
+ LOG.info("Second COB Run Time: {} ms ({} ms/loan)",
metrics.get("secondCOBTimeMs"),
+ ((Long) metrics.get("secondCOBTimeMs")) / loanCount);
+ LOG.info("----------------------------------------------------\n");
+
+ LOG.info("Full consolidated report will be printed after all tests
complete.");
+ }
+}