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 c1ec3d7fc FINERACT-2194: Batch jobs arre failing in a multitenant
environment
c1ec3d7fc is described below
commit c1ec3d7fc7c08419a118d0cbf1bdf2afbf7914ca
Author: Arnold Galovics <[email protected]>
AuthorDate: Wed Feb 26 22:51:34 2025 +0100
FINERACT-2194: Batch jobs arre failing in a multitenant environment
---
.../jobs/TenantAwareEqualsHashCodeAdvice.java | 61 ++++++++++++
.../scope/context/JobSynchronizationManager.java | 67 +++++++++++++
.../scope/context/StepSynchronizationManager.java | 67 +++++++++++++
.../service/AsyncLoanCOBExecutorServiceImpl.java | 10 +-
.../async/CustomAsyncExceptionHandler.java | 2 +-
.../jobs/service/JobRegisterServiceImpl.java | 2 +-
.../infrastructure/jobs/service/JobStarter.java | 54 ++++++++---
.../jobs/service/SchedulerJobListener.java | 106 ++++++++++-----------
.../jobs/service/SchedulerTriggerListener.java | 10 +-
.../ClasspathDuplicatesStepDefinitions.java | 2 +
.../jobs/service/JobStarterTest.java | 25 ++++-
11 files changed, 326 insertions(+), 80 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/TenantAwareEqualsHashCodeAdvice.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/TenantAwareEqualsHashCodeAdvice.java
new file mode 100644
index 000000000..9a3e295b5
--- /dev/null
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/TenantAwareEqualsHashCodeAdvice.java
@@ -0,0 +1,61 @@
+/**
+ * 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.infrastructure.jobs;
+
+import java.lang.reflect.Method;
+import java.util.Objects;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.springframework.cglib.proxy.Factory;
+import org.springframework.cglib.proxy.MethodInterceptor;
+import org.springframework.cglib.proxy.MethodProxy;
+
+public class TenantAwareEqualsHashCodeAdvice implements MethodInterceptor {
+
+ private final Object target;
+ private final String tenantIdentifier;
+
+ public TenantAwareEqualsHashCodeAdvice(Object target) {
+ this.target = target;
+ FineractPlatformTenant tenant = ThreadLocalContextUtil.getTenant();
+ this.tenantIdentifier = tenant != null ? tenant.getTenantIdentifier()
: null;
+ }
+
+ @Override
+ public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
+ String methodName = method.getName();
+
+ if ("equals".equals(methodName) && args.length == 1) {
+ Object other = args[0];
+
+ if (other instanceof Factory) {
+
+ TenantAwareEqualsHashCodeAdvice otherProxy =
(TenantAwareEqualsHashCodeAdvice) ((Factory) other).getCallback(0);
+ return Objects.equals(target, otherProxy.target) &&
Objects.equals(tenantIdentifier, otherProxy.tenantIdentifier);
+ }
+ return false;
+ }
+
+ if ("hashCode".equals(methodName) && args.length == 0) {
+ return Objects.hash(target.hashCode(), tenantIdentifier);
+ }
+
+ return proxy.invoke(target, args);
+ }
+}
diff --git
a/fineract-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java
b/fineract-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java
new file mode 100644
index 000000000..c4446ee5e
--- /dev/null
+++
b/fineract-core/src/main/java/org/springframework/batch/core/scope/context/JobSynchronizationManager.java
@@ -0,0 +1,67 @@
+/**
+ * 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.springframework.batch.core.scope.context;
+
+import org.apache.fineract.infrastructure.jobs.TenantAwareEqualsHashCodeAdvice;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobInstance;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.cglib.proxy.Enhancer;
+import org.springframework.lang.Nullable;
+
+// Temporary solution until spring-batch fixes the concurrency issue
+// https://github.com/spring-projects/spring-batch/issues/4774
+// Mostly copy from spring-batch
+@SuppressWarnings({ "HideUtilityClassConstructor" })
+public class JobSynchronizationManager {
+
+ private static final SynchronizationManagerSupport<JobExecution,
JobContext> manager = new SynchronizationManagerSupport<>() {
+
+ @Override
+ protected JobContext createNewContext(JobExecution execution) {
+ return new JobContext(execution);
+ }
+
+ @Override
+ protected void close(JobContext context) {
+ context.close();
+ }
+ };
+
+ @Nullable
+ public static JobContext getContext() {
+ return manager.getContext();
+ }
+
+ public static JobContext register(JobExecution jobExecution) {
+ Enhancer enhancer = new Enhancer();
+ enhancer.setSuperclass(JobExecution.class);
+ enhancer.setCallback(new
TenantAwareEqualsHashCodeAdvice(jobExecution));
+ return manager.register((JobExecution) enhancer.create(new Class[] {
JobInstance.class, Long.class, JobParameters.class },
+ new Object[] { jobExecution.getJobInstance(),
jobExecution.getId(), jobExecution.getJobParameters() }));
+ }
+
+ public static void close() {
+ manager.close();
+ }
+
+ public static void release() {
+ manager.release();
+ }
+}
diff --git
a/fineract-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java
b/fineract-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java
new file mode 100644
index 000000000..ca9e995e9
--- /dev/null
+++
b/fineract-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java
@@ -0,0 +1,67 @@
+/**
+ * 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.springframework.batch.core.scope.context;
+
+import org.apache.fineract.infrastructure.jobs.TenantAwareEqualsHashCodeAdvice;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.StepExecution;
+import org.springframework.cglib.proxy.Enhancer;
+import org.springframework.lang.Nullable;
+
+// Temporary solution until spring-batch fixes the concurrency issue
+// https://github.com/spring-projects/spring-batch/issues/4774
+// Mostly copy from spring-batch
+@SuppressWarnings({ "HideUtilityClassConstructor" })
+public class StepSynchronizationManager {
+
+ private static final SynchronizationManagerSupport<StepExecution,
StepContext> manager = new SynchronizationManagerSupport<>() {
+
+ @Override
+ protected StepContext createNewContext(StepExecution execution) {
+ return new StepContext(execution);
+ }
+
+ @Override
+ protected void close(StepContext context) {
+ context.close();
+ }
+ };
+
+ @Nullable
+ public static StepContext getContext() {
+ return manager.getContext();
+ }
+
+ public static StepContext register(StepExecution stepExecution) {
+ Enhancer enhancer = new Enhancer();
+ enhancer.setSuperclass(StepExecution.class);
+ enhancer.setCallback(new
TenantAwareEqualsHashCodeAdvice(stepExecution));
+ return manager.register((StepExecution) enhancer.create(new Class[] {
String.class, JobExecution.class },
+ new Object[] { stepExecution.getStepName(),
stepExecution.getJobExecution() }));
+ }
+
+ public static void close() {
+ manager.close();
+ }
+
+ public static void release() {
+ manager.release();
+ }
+
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
index f4051256b..ed6c1428d 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/cob/service/AsyncLoanCOBExecutorServiceImpl.java
@@ -80,7 +80,7 @@ public class AsyncLoanCOBExecutorServiceImpl implements
AsyncLoanCOBExecutorServ
?
loanIdAndLastClosedBusinessDate.get(0).getLastClosedBusinessDate()
: cobBusinessDate;
if (DateUtils.isBefore(oldestCOBProcessedDate, cobBusinessDate)) {
-
executeLoanCOBDayByDayUntilCOBBusinessDate(oldestCOBProcessedDate,
cobBusinessDate);
+
executeLoanCOBDayByDayUntilCOBBusinessDate(oldestCOBProcessedDate,
cobBusinessDate, context);
}
} catch (NoSuchJobException e) {
// Throwing an error here is useless as it will be swallowed hence
it is async method
@@ -94,19 +94,21 @@ public class AsyncLoanCOBExecutorServiceImpl implements
AsyncLoanCOBExecutorServ
}
}
- private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate
oldestCOBProcessedDate, LocalDate cobBusinessDate)
- throws NoSuchJobException, JobInstanceAlreadyCompleteException,
JobExecutionAlreadyRunningException,
+ private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate
oldestCOBProcessedDate, LocalDate cobBusinessDate,
+ FineractContext context) throws NoSuchJobException,
JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException,
JobParametersInvalidException, JobRestartException,
JobExecutionException {
Job job = jobLocator.getJob(LoanCOBConstant.JOB_NAME);
ScheduledJobDetail scheduledJobDetail =
scheduledJobDetailRepository.findByJobName(LoanCOBConstant.JOB_HUMAN_READABLE_NAME);
LocalDate executingBusinessDate = oldestCOBProcessedDate.plusDays(1);
while (!DateUtils.isAfter(executingBusinessDate, cobBusinessDate)) {
+ // Need to reinitialize the thread-local tenant info because after
running the job, it resets the thread
+ ThreadLocalContextUtil.init(context);
JobParameterDTO jobParameterDTO = new
JobParameterDTO(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME,
executingBusinessDate.format(DateTimeFormatter.ISO_DATE));
JobParameterDTO jobParameterCatchUpDTO = new
JobParameterDTO(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, "true");
Set<JobParameterDTO> jobParameters = new HashSet<>();
Collections.addAll(jobParameters, jobParameterDTO,
jobParameterCatchUpDTO);
- jobStarter.run(job, scheduledJobDetail, jobParameters);
+ jobStarter.run(job, scheduledJobDetail, jobParameters,
ThreadLocalContextUtil.getTenant().getTenantIdentifier());
executingBusinessDate = executingBusinessDate.plusDays(1);
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java
index a96305f76..9a40289f9 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/async/CustomAsyncExceptionHandler.java
@@ -28,7 +28,7 @@ public class CustomAsyncExceptionHandler implements
AsyncUncaughtExceptionHandle
@Override
public void handleUncaughtException(Throwable throwable, Method method,
Object... obj) {
- log.error("Exception message - {}", throwable.getMessage());
+ log.error("Exception", throwable);
log.error("Method name - {}", method.getName());
for (Object param : obj) {
log.error("Parameter value - {}", param);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java
index b9dd7f26f..25fd77311 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java
@@ -349,7 +349,7 @@ public class JobRegisterServiceImpl implements
JobRegisterService, ApplicationLi
jobDetailFactoryBean.setGroup(scheduledJobDetail.getGroupName());
jobDetailFactoryBean.setConcurrent(false);
- jobDetailFactoryBean.setArguments(job, scheduledJobDetail,
jobParameterDTOSet);
+ jobDetailFactoryBean.setArguments(job, scheduledJobDetail,
jobParameterDTOSet, tenant.getTenantIdentifier());
jobDetailFactoryBean.afterPropertiesSet();
return jobDetailFactoryBean.getObject();
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java
index 031dc0737..a90fb9029 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobStarter.java
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.infrastructure.jobs.service;
+import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -27,11 +28,19 @@ import java.util.Set;
import java.util.stream.Collectors;
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;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import
org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
import org.apache.fineract.infrastructure.jobs.domain.JobParameterRepository;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameService;
import
org.apache.fineract.infrastructure.jobs.service.jobparameterprovider.JobParameterProvider;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
import org.quartz.JobExecutionException;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.Job;
@@ -45,6 +54,8 @@ import org.springframework.batch.core.launch.JobLauncher;
import
org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import
org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
+import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
@Component
@@ -57,24 +68,41 @@ public class JobStarter {
private final JobParameterRepository jobParameterRepository;
private final List<JobParameterProvider<?>> jobParameterProviders;
private final JobNameService jobNameService;
+ private final TenantDetailsService tenantDetailsService;
+ private final AppUserRepositoryWrapper userRepository;
+ private final BusinessDateReadPlatformService
businessDateReadPlatformService;
public static final List<BatchStatus> FAILED_STATUSES =
List.of(BatchStatus.FAILED, BatchStatus.ABANDONED, BatchStatus.STOPPED,
BatchStatus.STOPPING, BatchStatus.UNKNOWN);
- public JobExecution run(Job job, ScheduledJobDetail scheduledJobDetail,
Set<JobParameterDTO> jobParameterDTOSet)
- throws JobInstanceAlreadyCompleteException,
JobExecutionAlreadyRunningException, JobParametersInvalidException,
- JobRestartException, JobExecutionException {
- Map<String, JobParameter<?>> jobParameterMap =
getJobParameter(scheduledJobDetail);
- JobParameters jobParameters = new
JobParametersBuilder(jobExplorer).getNextJobParameters(job)
- .addJobParameters(new JobParameters(jobParameterMap))
- .addJobParameters(new JobParameters(provideCustomJobParameters(
-
jobNameService.getJobByHumanReadableName(scheduledJobDetail.getJobName()).getEnumStyleName(),
jobParameterDTOSet)))
- .toJobParameters();
- JobExecution result = jobLauncher.run(job, jobParameters);
- if (FAILED_STATUSES.contains(result.getStatus())) {
- throw new JobExecutionException(result.getExitStatus().toString());
+ public JobExecution run(Job job, ScheduledJobDetail scheduledJobDetail,
Set<JobParameterDTO> jobParameterDTOSet,
+ String tenantIdentifier) throws
JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException,
+ JobParametersInvalidException, JobRestartException,
JobExecutionException {
+ try {
+ FineractPlatformTenant tenant =
tenantDetailsService.loadTenantById(tenantIdentifier);
+ ThreadLocalContextUtil.setTenant(tenant);
+ AppUser user = this.userRepository.fetchSystemUser();
+ UsernamePasswordAuthenticationToken auth = new
UsernamePasswordAuthenticationToken(user, user.getPassword(),
+ user.getAuthorities());
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ HashMap<BusinessDateType, LocalDate> businessDates =
businessDateReadPlatformService.getBusinessDates();
+ ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+ ThreadLocalContextUtil.setBusinessDates(businessDates);
+ Map<String, JobParameter<?>> jobParameterMap =
getJobParameter(scheduledJobDetail);
+ JobParameters jobParameters = new
JobParametersBuilder(jobExplorer).getNextJobParameters(job)
+ .addJobParameters(new JobParameters(jobParameterMap))
+ .addJobParameters(new
JobParameters(provideCustomJobParameters(
+
jobNameService.getJobByHumanReadableName(scheduledJobDetail.getJobName()).getEnumStyleName(),
+ jobParameterDTOSet)))
+ .toJobParameters();
+ JobExecution result = jobLauncher.run(job, jobParameters);
+ if (FAILED_STATUSES.contains(result.getStatus())) {
+ throw new
JobExecutionException(result.getExitStatus().toString());
+ }
+ return result;
+ } finally {
+ ThreadLocalContextUtil.reset();
}
- return result;
}
protected Map<String, org.springframework.batch.core.JobParameter<?>>
getJobParameter(ScheduledJobDetail scheduledJobDetail) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobListener.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobListener.java
index 6278eba1c..00b169169 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobListener.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/SchedulerJobListener.java
@@ -18,25 +18,20 @@
*/
package org.apache.fineract.infrastructure.jobs.service;
-import java.time.LocalDate;
import java.util.Date;
-import java.util.HashMap;
import lombok.RequiredArgsConstructor;
-import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import
org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
-import org.apache.fineract.infrastructure.core.domain.ActionContext;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import
org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
import org.apache.fineract.infrastructure.jobs.domain.ScheduledJobRunHistory;
-import org.apache.fineract.useradministration.domain.AppUser;
import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.JobListener;
import org.quartz.Trigger;
-import
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
-import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/**
@@ -50,6 +45,7 @@ public class SchedulerJobListener implements JobListener {
private final SchedularWritePlatformService schedularService;
private final AppUserRepositoryWrapper userRepository;
private final BusinessDateReadPlatformService
businessDateReadPlatformService;
+ private final TenantDetailsService tenantDetailsService;
private int stackTraceLevel = 0;
@Override
@@ -58,65 +54,63 @@ public class SchedulerJobListener implements JobListener {
}
@Override
- public void jobToBeExecuted(@SuppressWarnings("unused") final
JobExecutionContext context) {
- AppUser user = this.userRepository.fetchSystemUser();
- UsernamePasswordAuthenticationToken auth = new
UsernamePasswordAuthenticationToken(user, user.getPassword(),
user.getAuthorities());
- SecurityContextHolder.getContext().setAuthentication(auth);
- HashMap<BusinessDateType, LocalDate> businessDates =
businessDateReadPlatformService.getBusinessDates();
- ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
- ThreadLocalContextUtil.setBusinessDates(businessDates);
- }
+ public void jobToBeExecuted(@SuppressWarnings("unused") final
JobExecutionContext context) {}
@Override
- public void jobExecutionVetoed(@SuppressWarnings("unused") final
JobExecutionContext context) {
-
- }
+ public void jobExecutionVetoed(@SuppressWarnings("unused") final
JobExecutionContext context) {}
@Override
public void jobWasExecuted(final JobExecutionContext context, final
JobExecutionException jobException) {
- final Trigger trigger = context.getTrigger();
- final JobKey key = context.getJobDetail().getKey();
- final String jobKey = key.getName() +
SchedulerServiceConstants.JOB_KEY_SEPERATOR + key.getGroup();
- final ScheduledJobDetail scheduledJobDetails =
this.schedularService.findByJobKey(jobKey);
- final Long version = this.schedularService.fetchMaxVersionBy(jobKey) +
1;
- String status = SchedulerServiceConstants.STATUS_SUCCESS;
- String errorMessage = null;
- String errorLog = null;
- if (jobException != null) {
- status = SchedulerServiceConstants.STATUS_FAILED;
- this.stackTraceLevel = 0;
- final Throwable throwable = getCauseFromException(jobException);
- this.stackTraceLevel = 0;
- StackTraceElement[] stackTraceElements = null;
- errorMessage = throwable.getMessage();
- stackTraceElements = throwable.getStackTrace();
- final StringBuilder sb = new StringBuilder(throwable.toString());
- for (final StackTraceElement element : stackTraceElements) {
- sb.append("\n \t at
").append(element.getClassName()).append(".").append(element.getMethodName()).append("(")
- .append(element.getLineNumber()).append(")");
- }
- errorLog = sb.toString();
+ try {
+ String tenantIdentifier =
context.getMergedJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
+ FineractPlatformTenant tenant =
tenantDetailsService.loadTenantById(tenantIdentifier);
+ ThreadLocalContextUtil.setTenant(tenant);
+ final Trigger trigger = context.getTrigger();
- }
- String triggerType = SchedulerServiceConstants.TRIGGER_TYPE_CRON;
- if
(context.getMergedJobDataMap().containsKey(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE))
{
- triggerType =
context.getMergedJobDataMap().getString(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE);
- }
- if (SchedulerServiceConstants.TRIGGER_TYPE_CRON.equals(triggerType) &&
trigger.getNextFireTime() != null
- &&
trigger.getNextFireTime().after(scheduledJobDetails.getNextRunTime())) {
- scheduledJobDetails.setNextRunTime(trigger.getNextFireTime());
- }
+ final JobKey key = context.getJobDetail().getKey();
+ final String jobKey = key.getName() +
SchedulerServiceConstants.JOB_KEY_SEPERATOR + key.getGroup();
+ final ScheduledJobDetail scheduledJobDetails =
this.schedularService.findByJobKey(jobKey);
+ final Long version =
this.schedularService.fetchMaxVersionBy(jobKey) + 1;
+ String status = SchedulerServiceConstants.STATUS_SUCCESS;
+ String errorMessage = null;
+ String errorLog = null;
+ if (jobException != null) {
+ status = SchedulerServiceConstants.STATUS_FAILED;
+ this.stackTraceLevel = 0;
+ final Throwable throwable =
getCauseFromException(jobException);
+ this.stackTraceLevel = 0;
+ StackTraceElement[] stackTraceElements = null;
+ errorMessage = throwable.getMessage();
+ stackTraceElements = throwable.getStackTrace();
+ final StringBuilder sb = new
StringBuilder(throwable.toString());
+ for (final StackTraceElement element : stackTraceElements) {
+ sb.append("\n \t at
").append(element.getClassName()).append(".").append(element.getMethodName()).append("(")
+ .append(element.getLineNumber()).append(")");
+ }
+ errorLog = sb.toString();
- scheduledJobDetails.setPreviousRunStartTime(context.getFireTime());
- scheduledJobDetails.setCurrentlyRunning(false);
+ }
+ String triggerType = SchedulerServiceConstants.TRIGGER_TYPE_CRON;
+ if
(context.getMergedJobDataMap().containsKey(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE))
{
+ triggerType =
context.getMergedJobDataMap().getString(SchedulerServiceConstants.TRIGGER_TYPE_REFERENCE);
+ }
+ if
(SchedulerServiceConstants.TRIGGER_TYPE_CRON.equals(triggerType) &&
trigger.getNextFireTime() != null
+ &&
trigger.getNextFireTime().after(scheduledJobDetails.getNextRunTime())) {
+ scheduledJobDetails.setNextRunTime(trigger.getNextFireTime());
+ }
- final ScheduledJobRunHistory runHistory = new
ScheduledJobRunHistory().setScheduledJobDetail(scheduledJobDetails)
-
.setVersion(version).setStartTime(context.getFireTime()).setEndTime(new
Date()).setStatus(status)
-
.setErrorMessage(errorMessage).setTriggerType(triggerType).setErrorLog(errorLog);
- // scheduledJobDetails.addRunHistory(runHistory);
+ scheduledJobDetails.setPreviousRunStartTime(context.getFireTime());
+ scheduledJobDetails.setCurrentlyRunning(false);
- this.schedularService.saveOrUpdate(scheduledJobDetails, runHistory);
+ final ScheduledJobRunHistory runHistory = new
ScheduledJobRunHistory().setScheduledJobDetail(scheduledJobDetails)
+
.setVersion(version).setStartTime(context.getFireTime()).setEndTime(new
Date()).setStatus(status)
+
.setErrorMessage(errorMessage).setTriggerType(triggerType).setErrorLog(errorLog);
+ // scheduledJobDetails.addRunHistory(runHistory);
+ this.schedularService.saveOrUpdate(scheduledJobDetails,
runHistory);
+ } finally {
+ ThreadLocalContextUtil.reset();
+ }
}
private Throwable getCauseFromException(final Throwable exception) {
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 a70fbcae9..ead4e9ead 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
@@ -52,9 +52,13 @@ public class SchedulerTriggerListener implements
TriggerListener {
public boolean vetoJobExecution(final Trigger trigger, final
JobExecutionContext context) {
String tenantIdentifier =
trigger.getJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
FineractPlatformTenant tenant =
tenantDetailsService.loadTenantById(tenantIdentifier);
- ThreadLocalContextUtil.setTenant(tenant);
- ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
- return schedulerVetoer.veto(trigger, context);
+ try {
+ ThreadLocalContextUtil.setTenant(tenant);
+ ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+ return schedulerVetoer.veto(trigger, context);
+ } finally {
+ ThreadLocalContextUtil.reset();
+ }
}
@Override
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java
index 5936c7594..0d056ca1d 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/classpath/ClasspathDuplicatesStepDefinitions.java
@@ -128,6 +128,8 @@ public class ClasspathDuplicatesStepDefinitions implements
En {
|| resourcePath.startsWith("META-INF/terracotta") ||
resourcePath.startsWith("com/fasterxml/jackson/core/io/doubleparser")
// Groovy is groovy
|| resourcePath.startsWith("META-INF/groovy")
+ ||
resourcePath.startsWith("org/springframework/batch/core/scope/context/JobSynchronizationManager")
+ ||
resourcePath.startsWith("org/springframework/batch/core/scope/context/StepSynchronizationManager")
// Something doesn't to be a perfectly clean in Maven Surefire:
|| resourcePath.startsWith("META-INF/maven/") ||
resourcePath.contains("surefire")
// org.slf4j.impl.StaticLoggerBinder.class in testutils for the
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/service/JobStarterTest.java
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/service/JobStarterTest.java
index a9bec4171..4dea58338 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/service/JobStarterTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/service/JobStarterTest.java
@@ -19,14 +19,19 @@
package org.apache.fineract.infrastructure.jobs.service;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
+import
org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import
org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
import org.apache.fineract.infrastructure.jobs.data.JobParameterDTO;
import org.apache.fineract.infrastructure.jobs.domain.JobParameter;
import org.apache.fineract.infrastructure.jobs.domain.JobParameterRepository;
@@ -34,6 +39,8 @@ import
org.apache.fineract.infrastructure.jobs.domain.ScheduledJobDetail;
import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameData;
import org.apache.fineract.infrastructure.jobs.service.jobname.JobNameService;
import
org.apache.fineract.infrastructure.jobs.service.jobparameterprovider.JobParameterProvider;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.apache.fineract.useradministration.domain.AppUserRepositoryWrapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -73,6 +80,14 @@ public class JobStarterTest {
private List<JobParameterProvider<?>> jobParameterProviders;
@Mock
private JobNameService jobNameService;
+
+ @Mock
+ private TenantDetailsService tenantDetailsService;
+ @Mock
+ private AppUserRepositoryWrapper userRepository;
+ @Mock
+ private BusinessDateReadPlatformService businessDateReadPlatformService;
+
@Captor
private ArgumentCaptor<Set<JobParameterDTO>> jobParameterDTOCaptor;
@@ -106,7 +121,7 @@ public class JobStarterTest {
ScheduledJobDetail scheduledJobDetail =
Mockito.mock(ScheduledJobDetail.class);
when(jobExecution.getStatus()).thenReturn(BatchStatus.COMPLETED);
setupMocks(jobExecution, job, scheduledJobDetail);
- JobExecution result = underTest.run(job, scheduledJobDetail, Set.of());
+ JobExecution result = underTest.run(job, scheduledJobDetail, Set.of(),
"default");
Assertions.assertEquals(jobExecution, result);
}
@@ -122,7 +137,7 @@ public class JobStarterTest {
when(jobExecution.getStatus()).thenReturn(BatchStatus.FAILED);
when(jobExecution.getExitStatus()).thenReturn(new
ExitStatus(failedStatus.name(), "testException"));
JobExecutionException exception =
Assertions.assertThrows(JobExecutionException.class,
- () -> underTest.run(job, scheduledJobDetail, Set.of()));
+ () -> underTest.run(job, scheduledJobDetail, Set.of(),
"default"));
Assertions.assertEquals(String.format("exitCode=%s;exitDescription=%s",
failedStatus.name(), "testException"),
exception.getMessage());
}
@@ -140,5 +155,11 @@ public class JobStarterTest {
when(jobParameterProvider.canProvideParametersForJob("testJobName")).thenReturn(true);
when(jobParameterProviders.stream()).thenReturn(Stream.of(jobParameterProvider));
when(jobNameService.getJobByHumanReadableName(any(String.class))).thenReturn(new
JobNameData("testEnumstyleName", "testHumanReadableName"));
+
when(tenantDetailsService.loadTenantById(anyString())).thenReturn(FineractPlatformTenant.builder().build());
+ AppUser appUser = Mockito.mock(AppUser.class);
+ when(appUser.getPassword()).thenReturn("");
+ when(appUser.getAuthorities()).thenReturn(List.of());
+ when(userRepository.fetchSystemUser()).thenReturn(appUser);
+
when(businessDateReadPlatformService.getBusinessDates()).thenReturn(new
HashMap<>());
}
}