You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2023/02/08 19:21:31 UTC
[fineract] branch develop updated: FINERACT-1724: Fixed Generate Loss Loan provisioning job + enhanced the stuck job listener to be smarter
This is an automated email from the ASF dual-hosted git repository.
arnold 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 0e96b71f0 FINERACT-1724: Fixed Generate Loss Loan provisioning job + enhanced the stuck job listener to be smarter
0e96b71f0 is described below
commit 0e96b71f0ef3514a4b75caecaea53a39e560fe8b
Author: Arnold Galovics <ga...@gmail.com>
AuthorDate: Wed Feb 8 16:44:57 2023 +0100
FINERACT-1724: Fixed Generate Loss Loan provisioning job + enhanced the stuck job listener to be smarter
---
.../core/config/FineractProperties.java | 9 ++
.../jobs/domain/JobExecutionRepository.java | 174 +++++++++++++++------
.../jobs/service/StuckJobExecutorService.java | 2 +-
.../jobs/service/StuckJobExecutorServiceImpl.java | 32 ++--
.../jobs/service/StuckJobListener.java | 16 +-
.../GenerateLoanlossProvisioningTasklet.java | 16 +-
.../src/main/resources/application.properties | 2 +
.../GenerateLoanlossProvisioningTaskletTest.java | 137 ++++++++++++++++
8 files changed, 308 insertions(+), 80 deletions(-)
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
index 85fbef4b7..9a2299d9d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
@@ -50,6 +50,8 @@ public class FineractProperties {
private FineractReportProperties report;
+ private FineractJobProperties job;
+
@Getter
@Setter
public static class FineractTenantProperties {
@@ -225,4 +227,11 @@ public class FineractProperties {
private String bucketName;
private Boolean enabled;
}
+
+ @Getter
+ @Setter
+ public static class FineractJobProperties {
+
+ private int stuckRetryThreshold;
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java
index 016a92b0f..0b4753059 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/domain/JobExecutionRepository.java
@@ -22,10 +22,12 @@ import static org.springframework.batch.core.BatchStatus.COMPLETED;
import static org.springframework.batch.core.BatchStatus.FAILED;
import static org.springframework.batch.core.BatchStatus.STARTED;
import static org.springframework.batch.core.BatchStatus.STARTING;
+import static org.springframework.batch.core.BatchStatus.UNKNOWN;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
@@ -34,68 +36,148 @@ import org.springframework.stereotype.Component;
public class JobExecutionRepository {
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+ private final FineractProperties fineractProperties;
public List<String> getStuckJobNames(NamedParameterJdbcTemplate jdbcTemplate) {
- return jdbcTemplate.queryForList(
- "SELECT bji.JOB_NAME as STUCK_JOB_NAME FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "WHERE bje.STATUS IN (:statuses) AND bje.JOB_INSTANCE_ID NOT IN ("
- + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "WHERE bje.STATUS = :completedStatus)",
- Map.of("statuses", List.of(STARTED.name(), FAILED.name()), "completedStatus", COMPLETED.name()), String.class);
+ int threshold = fineractProperties.getJob().getStuckRetryThreshold();
+ return jdbcTemplate.queryForList("""
+ SELECT DISTINCT(bji.JOB_NAME) as STUCK_JOB_NAME
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ WHERE
+ bje.STATUS IN (:statuses)
+ AND
+ bje.JOB_INSTANCE_ID NOT IN (
+ SELECT bje.JOB_INSTANCE_ID
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ WHERE bje.STATUS IN (:completedStatuses)
+ )
+ GROUP BY BJI.JOB_INSTANCE_ID
+ HAVING COUNT(BJI.JOB_INSTANCE_ID) <= :threshold
+ """, Map.of("statuses", List.of(STARTED.name()), "completedStatuses",
+ List.of(COMPLETED.name(), FAILED.name(), UNKNOWN.name()), "threshold", threshold), String.class);
}
public Long getStuckJobCountByJobName(String jobName) {
- return namedParameterJdbcTemplate.queryForObject(
- "SELECT COUNT(*) as STUCK_JOB_COUNT FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bje.JOB_INSTANCE_ID NOT IN ("
- + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)",
- Map.of("statuses", List.of(STARTED.name(), FAILED.name()), "jobName", jobName, "completedStatus", COMPLETED.name()),
- Long.class);
+ int threshold = fineractProperties.getJob().getStuckRetryThreshold();
+ return namedParameterJdbcTemplate.queryForObject("""
+ SELECT COUNT(DISTINCT bji.JOB_NAME) as STUCK_JOB_COUNT
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ WHERE
+ bje.STATUS IN (:statuses)
+ AND
+ bji.JOB_NAME = :jobName
+ AND
+ bje.JOB_INSTANCE_ID NOT IN (
+ SELECT bje.JOB_INSTANCE_ID
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ WHERE
+ bje.STATUS IN (:completedStatuses)
+ AND
+ bji.JOB_NAME = :jobName
+ )
+ GROUP BY BJI.JOB_INSTANCE_ID
+ HAVING COUNT(BJI.JOB_INSTANCE_ID) <= :threshold
+ """, Map.of("statuses", List.of(STARTED.name()), "jobName", jobName, "completedStatuses",
+ List.of(COMPLETED.name(), FAILED.name(), UNKNOWN.name()), "threshold", threshold), Long.class);
}
public List<Long> getStuckJobIdsByJobName(String jobName) {
- return namedParameterJdbcTemplate.queryForList(
- "SELECT bje.JOB_EXECUTION_ID FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bje.JOB_INSTANCE_ID NOT IN ("
- + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)",
- Map.of("statuses", List.of(STARTED.name(), FAILED.name()), "jobName", jobName, "completedStatus", COMPLETED.name()),
- Long.class);
+ int threshold = fineractProperties.getJob().getStuckRetryThreshold();
+ return namedParameterJdbcTemplate.queryForList("""
+ SELECT bje.JOB_EXECUTION_ID
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ WHERE
+ bje.STATUS IN (:statuses)
+ AND
+ bji.JOB_NAME = :jobName
+ AND
+ bje.JOB_INSTANCE_ID NOT IN (
+ SELECT bje.JOB_INSTANCE_ID
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ WHERE
+ bje.STATUS IN (:completedStatuses)
+ AND
+ bji.JOB_NAME = :jobName
+ )
+ GROUP BY BJI.JOB_INSTANCE_ID, BJE.JOB_EXECUTION_ID
+ HAVING COUNT(BJI.JOB_INSTANCE_ID) <= :threshold
+ """, Map.of("statuses", List.of(STARTED.name()), "jobName", jobName, "completedStatuses",
+ List.of(COMPLETED.name(), FAILED.name(), UNKNOWN.name()), "threshold", threshold), Long.class);
}
public Long getNotCompletedPartitionsCount(Long jobExecutionId, String partitionerStepName) {
- return namedParameterJdbcTemplate.queryForObject(
- "SELECT COUNT(bse.STEP_EXECUTION_ID) FROM BATCH_STEP_EXECUTION bse "
- + "WHERE bse.JOB_EXECUTION_ID = :jobExecutionId AND bse.STEP_NAME <> :stepName AND bse.status <> :status",
- Map.of("jobExecutionId", jobExecutionId, "stepName", partitionerStepName, "status", COMPLETED.name()), Long.class);
+ return namedParameterJdbcTemplate.queryForObject("""
+ SELECT COUNT(bse.STEP_EXECUTION_ID)
+ FROM BATCH_STEP_EXECUTION bse
+ WHERE
+ bse.JOB_EXECUTION_ID = :jobExecutionId
+ AND
+ bse.STEP_NAME <> :stepName
+ AND
+ bse.status <> :status
+ """, Map.of("jobExecutionId", jobExecutionId, "stepName", partitionerStepName, "status", COMPLETED.name()), Long.class);
}
public void updateJobStatusToFailed(Long stuckJobId, String partitionerStepName) {
- namedParameterJdbcTemplate.update(
- "UPDATE BATCH_STEP_EXECUTION SET STATUS = :status WHERE JOB_EXECUTION_ID = :jobExecutionId AND STEP_NAME = :stepName",
- Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId, "stepName", partitionerStepName));
- namedParameterJdbcTemplate.update(
- "UPDATE BATCH_JOB_EXECUTION SET STATUS = :status, START_TIME = null, END_TIME = null WHERE JOB_EXECUTION_ID = :jobExecutionId",
- Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId));
+ namedParameterJdbcTemplate.update("""
+ UPDATE BATCH_STEP_EXECUTION
+ SET STATUS = :status
+ WHERE
+ JOB_EXECUTION_ID = :jobExecutionId
+ AND
+ STEP_NAME = :stepName
+ """, Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId, "stepName", partitionerStepName));
+ namedParameterJdbcTemplate.update("""
+ UPDATE BATCH_JOB_EXECUTION
+ SET
+ STATUS = :status,
+ START_TIME = null,
+ END_TIME = null
+ WHERE
+ JOB_EXECUTION_ID = :jobExecutionId
+ """, Map.of("status", FAILED.name(), "jobExecutionId", stuckJobId));
}
public List<Long> getRunningJobsByExecutionParameter(String jobName, String parameterKeyName, String parameterValue) {
- return namedParameterJdbcTemplate.queryForList("SELECT bje.JOB_EXECUTION_ID FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "INNER JOIN BATCH_JOB_EXECUTION_PARAMS bjep ON bje.JOB_EXECUTION_ID = bjep.JOB_EXECUTION_ID "
- + "WHERE bje.STATUS IN (:statuses) AND bji.JOB_NAME = :jobName AND bjep.KEY_NAME = :parameterKeyName AND bjep.STRING_VAL = :parameterValue AND bje.JOB_INSTANCE_ID NOT IN ("
- + "SELECT bje.JOB_INSTANCE_ID FROM BATCH_JOB_INSTANCE bji "
- + "INNER JOIN BATCH_JOB_EXECUTION bje ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID "
- + "WHERE bje.STATUS = :completedStatus AND bji.JOB_NAME = :jobName)",
- Map.of("statuses", List.of(STARTED.name(), STARTING.name()), "jobName", jobName, "completedStatus", COMPLETED.name(),
- "parameterKeyName", parameterKeyName, "parameterValue", parameterValue),
- Long.class);
+ return namedParameterJdbcTemplate.queryForList("""
+ SELECT bje.JOB_EXECUTION_ID
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ INNER JOIN BATCH_JOB_EXECUTION_PARAMS bjep
+ ON bje.JOB_EXECUTION_ID = bjep.JOB_EXECUTION_ID
+ WHERE
+ bje.STATUS IN (:statuses)
+ AND
+ bji.JOB_NAME = :jobName
+ AND
+ bjep.KEY_NAME = :parameterKeyName
+ AND
+ bjep.STRING_VAL = :parameterValue
+ AND
+ bje.JOB_INSTANCE_ID NOT IN (
+ SELECT bje.JOB_INSTANCE_ID
+ FROM BATCH_JOB_INSTANCE bji
+ INNER JOIN BATCH_JOB_EXECUTION bje
+ ON bji.JOB_INSTANCE_ID = bje.JOB_INSTANCE_ID
+ WHERE
+ bje.STATUS = :completedStatus
+ AND
+ bji.JOB_NAME = :jobName
+ )
+ """, Map.of("statuses", List.of(STARTED.name(), STARTING.name()), "jobName", jobName, "completedStatus", COMPLETED.name(),
+ "parameterKeyName", parameterKeyName, "parameterValue", parameterValue), Long.class);
}
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java
index 70e365308..8fe2c3346 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorService.java
@@ -20,5 +20,5 @@ package org.apache.fineract.infrastructure.jobs.service;
public interface StuckJobExecutorService {
- void executeStuckJob(String jobName, Long jobId);
+ void resumeStuckJob(String jobName);
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java
index d69aed9f5..5f46c88f7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobExecutorServiceImpl.java
@@ -25,7 +25,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.jobs.data.partitionedjobs.PartitionedJob;
import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository;
-import org.jetbrains.annotations.NotNull;
import org.springframework.batch.core.launch.JobOperator;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
@@ -40,20 +39,33 @@ public class StuckJobExecutorServiceImpl implements StuckJobExecutorService {
private final JobExecutionRepository jobExecutionRepository;
private final TransactionTemplate transactionTemplate;
private final JobOperator jobOperator;
- private final JobRegisterService jobRegisterService;
@Override
- public void executeStuckJob(String jobName, Long jobId) {
+ public void resumeStuckJob(String jobName) {
+ List<Long> stuckJobIds = getStuckJobIds(jobName);
if (isPartitionedJob(jobName) && areThereStuckJobs(jobName)) {
- // Restarting stuck Partitioned jobs
- List<Long> stuckJobIds = getStuckJobIds(jobName);
- stuckJobIds.forEach(stuckJobId -> handleStuckJob(stuckJobId, getPartitionerStepName(jobName)));
+ restartPartitionedJobs(jobName, stuckJobIds);
} else {
- // Executing stuck Tasklet jobs
- jobRegisterService.executeJobWithParameters(jobId, null);
+ restartTaskletJobs(stuckJobIds);
}
}
+ private void restartTaskletJobs(List<Long> stuckJobIds) {
+ stuckJobIds.forEach(this::handleStuckTaskletJob);
+ }
+
+ private void handleStuckTaskletJob(Long stuckJobId) {
+ try {
+ jobOperator.restart(stuckJobId);
+ } catch (Exception e) {
+ throw new RuntimeException("Exception while handling a stuck job", e);
+ }
+ }
+
+ private void restartPartitionedJobs(String jobName, List<Long> stuckJobIds) {
+ stuckJobIds.forEach(stuckJobId -> handleStuckPartitionedJob(stuckJobId, getPartitionerStepName(jobName)));
+ }
+
private boolean isPartitionedJob(String jobName) {
return PartitionedJob.existsByJobName(jobName);
}
@@ -71,14 +83,14 @@ public class StuckJobExecutorServiceImpl implements StuckJobExecutorService {
return jobExecutionRepository.getStuckJobIdsByJobName(jobName);
}
- private void handleStuckJob(Long stuckJobId, String partitionerStepName) {
+ private void handleStuckPartitionedJob(Long stuckJobId, String partitionerStepName) {
try {
waitUntilAllPartitionsFinished(stuckJobId, partitionerStepName);
transactionTemplate.setPropagationBehavior(PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
- protected void doInTransactionWithoutResult(@NotNull TransactionStatus status) {
+ protected void doInTransactionWithoutResult(TransactionStatus status) {
jobExecutionRepository.updateJobStatusToFailed(stuckJobId, partitionerStepName);
}
});
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java
index 948f7be59..353c5c914 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/StuckJobListener.java
@@ -21,8 +21,6 @@ package org.apache.fineract.infrastructure.jobs.service;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
-import java.util.Map;
-import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
@@ -31,7 +29,6 @@ import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.external.service.JdbcTemplateFactory;
import org.apache.fineract.infrastructure.jobs.domain.JobExecutionRepository;
-import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameData;
import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameProvider;
import org.apache.fineract.infrastructure.security.service.TenantDetailsService;
import org.springframework.batch.core.configuration.JobRegistry;
@@ -67,18 +64,9 @@ public class StuckJobListener implements ApplicationListener<ContextRefreshedEve
HashMap<BusinessDateType, LocalDate> businessDates = businessDateReadPlatformService.getBusinessDates();
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil.setBusinessDates(businessDates);
- stuckJobNames.forEach(stuckJobName -> {
- String sql = "select id from job where name = :jobName";
- Optional<JobNameData> jobNameDataOptional = jobNameProvider.provide().stream()
- .filter(jobNameData -> stuckJobName.equals(jobNameData.getEnumStyleName())).findFirst();
- JobNameData jobNameData = jobNameDataOptional
- .orElseThrow(() -> new IllegalArgumentException("Job not found by name: " + stuckJobName));
- Long stuckJobId = namedParameterJdbcTemplate.queryForObject(sql,
- Map.of("jobName", jobNameData.getHumanReadableName()), Long.class);
- stuckJobExecutorService.executeStuckJob(stuckJobName, stuckJobId);
- });
+ stuckJobNames.forEach(stuckJobExecutorService::resumeStuckJob);
} catch (Exception e) {
- throw new RuntimeException(e);
+ throw new RuntimeException("Error while trying to restart stuck jobs", e);
} finally {
ThreadLocalContextUtil.reset();
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java
index 2cb450608..a444c4542 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTasklet.java
@@ -22,6 +22,7 @@ import java.time.LocalDate;
import java.util.Collection;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.fineract.accounting.provisioning.exception.ProvisioningEntryAlreadyCreatedException;
import org.apache.fineract.accounting.provisioning.service.ProvisioningEntriesWritePlatformService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
@@ -31,8 +32,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.DataIntegrityViolationException;
-import org.springframework.orm.jpa.JpaSystemException;
@Slf4j
@RequiredArgsConstructor
@@ -48,14 +47,13 @@ public class GenerateLoanlossProvisioningTasklet implements Tasklet {
try {
Collection<ProvisioningCriteriaData> criteriaCollection = provisioningCriteriaReadPlatformService
.retrieveAllProvisioningCriterias();
- if (criteriaCollection == null || criteriaCollection.size() == 0) {
- throw new NoProvisioningCriteriaDefinitionFoundException();
+ if (CollectionUtils.isNotEmpty(criteriaCollection)) {
+ provisioningEntriesWritePlatformService.createProvisioningEntry(currentDate, addJournalEntries);
}
- provisioningEntriesWritePlatformService.createProvisioningEntry(currentDate, addJournalEntries);
- } catch (ProvisioningEntryAlreadyCreatedException peace) {
- log.error("Provisioning Entry already created", peace);
- } catch (final JpaSystemException | DataIntegrityViolationException dve) {
- log.error("Problem occurred in generateLoanLossProvisioningAmount function", dve);
+ } catch (ProvisioningEntryAlreadyCreatedException e) {
+ log.error("Provisioning entry already created", e);
+ } catch (Exception e) {
+ log.error("Problem occurred when generating provisioning entries", e);
}
return RepeatStatus.FINISHED;
}
diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties
index c7f8078ab..170ff1cee 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -43,6 +43,8 @@ fineract.mode.batch-manager-enabled=${FINERACT_MODE_BATCH_MANAGER_ENABLED:true}
fineract.correlation.enabled=${FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED:false}
fineract.correlation.header-name=${FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME:X-Correlation-ID}
+fineract.job.stuck-retry-threshold=${FINERACT_JOB_STUCK_RETRY_THRESHOLD:5}
+
fineract.partitioned-job.partitioned-job-properties[0].job-name=LOAN_COB
fineract.partitioned-job.partitioned-job-properties[0].chunk-size=${LOAN_COB_CHUNK_SIZE:100}
fineract.partitioned-job.partitioned-job-properties[0].partition-size=${LOAN_COB_PARTITION_SIZE:100}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTaskletTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTaskletTest.java
new file mode 100644
index 000000000..2a5462c5d
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/jobs/generateloanlossprovisioning/GenerateLoanlossProvisioningTaskletTest.java
@@ -0,0 +1,137 @@
+/**
+ * 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.portfolio.loanaccount.jobs.generateloanlossprovisioning;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.springframework.batch.repeat.RepeatStatus.FINISHED;
+
+import java.time.LocalDate;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.fineract.accounting.provisioning.exception.ProvisioningEntryAlreadyCreatedException;
+import org.apache.fineract.accounting.provisioning.service.ProvisioningEntriesWritePlatformService;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.organisation.provisioning.data.ProvisioningCriteriaData;
+import org.apache.fineract.organisation.provisioning.service.ProvisioningCriteriaReadPlatformService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.batch.core.StepContribution;
+import org.springframework.batch.core.scope.context.ChunkContext;
+import org.springframework.batch.repeat.RepeatStatus;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class GenerateLoanlossProvisioningTaskletTest {
+
+ public static final LocalDate BUSINESS_DATE = LocalDate.of(2022, 6, 12);
+
+ @Mock
+ private StepContribution stepContribution;
+
+ @Mock
+ private ChunkContext chunkContext;
+
+ @Mock
+ private ProvisioningCriteriaReadPlatformService provisioningCriteriaReadPlatformService;
+ @Mock
+ private ProvisioningEntriesWritePlatformService provisioningEntriesWritePlatformService;
+
+ @InjectMocks
+ private GenerateLoanlossProvisioningTasklet underTest;
+
+ @BeforeEach
+ public void setUp() {
+ ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, BUSINESS_DATE)));
+ }
+
+ @Test
+ public void testExecuteShouldCreateProvisioningEntry() throws Exception {
+ // given
+ Collection<ProvisioningCriteriaData> provisioningCriterias = List.of(new ProvisioningCriteriaData());
+ given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias);
+ // when
+ RepeatStatus result = underTest.execute(stepContribution, chunkContext);
+ // then
+ verify(provisioningEntriesWritePlatformService).createProvisioningEntry(BUSINESS_DATE, true);
+ assertThat(result).isEqualTo(FINISHED);
+ }
+
+ @Test
+ public void testExecuteShouldNotCreateProvisioningEntryWhenNoProvisioningCriteriasArePresent() throws Exception {
+ // given
+ Collection<ProvisioningCriteriaData> provisioningCriterias = List.of();
+ given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias);
+ // when
+ RepeatStatus result = underTest.execute(stepContribution, chunkContext);
+ // then
+ verifyNoInteractions(provisioningEntriesWritePlatformService);
+ assertThat(result).isEqualTo(FINISHED);
+ }
+
+ @Test
+ public void testExecuteShouldNotCreateProvisioningEntryWhenNullProvisioningCriteriasArePresent() throws Exception {
+ // given
+ given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(null);
+ // when
+ RepeatStatus result = underTest.execute(stepContribution, chunkContext);
+ // then
+ verifyNoInteractions(provisioningEntriesWritePlatformService);
+ assertThat(result).isEqualTo(FINISHED);
+ }
+
+ @Test
+ public void testExecuteShouldNotFailWhenProvisioningEntryIsAlreadyCreated() throws Exception {
+ // given
+ Collection<ProvisioningCriteriaData> provisioningCriterias = List.of(new ProvisioningCriteriaData());
+ given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias);
+ given(provisioningEntriesWritePlatformService.createProvisioningEntry(BUSINESS_DATE, true))
+ .willThrow(new ProvisioningEntryAlreadyCreatedException(1L, BUSINESS_DATE));
+ // when
+ RepeatStatus result = underTest.execute(stepContribution, chunkContext);
+ // then
+ verify(provisioningEntriesWritePlatformService).createProvisioningEntry(BUSINESS_DATE, true);
+ assertThat(result).isEqualTo(FINISHED);
+ }
+
+ @Test
+ public void testExecuteShouldNotFailWhenExceptionIsThrownInProvisioningEntryCreation() throws Exception {
+ // given
+ Collection<ProvisioningCriteriaData> provisioningCriterias = List.of(new ProvisioningCriteriaData());
+ given(provisioningCriteriaReadPlatformService.retrieveAllProvisioningCriterias()).willReturn(provisioningCriterias);
+ given(provisioningEntriesWritePlatformService.createProvisioningEntry(BUSINESS_DATE, true)).willThrow(new RuntimeException("Test"));
+ // when
+ RepeatStatus result = underTest.execute(stepContribution, chunkContext);
+ // then
+ verify(provisioningEntriesWritePlatformService).createProvisioningEntry(BUSINESS_DATE, true);
+ assertThat(result).isEqualTo(FINISHED);
+ }
+
+}