From 199ff6d7f1ac085935374042d1745d6a3cad7855 Mon Sep 17 00:00:00 2001 From: Arnold Galovics Date: Wed, 26 Feb 2025 22:51:34 +0100 Subject: [PATCH] FINERACT-2194: Batch jobs arre failing in a multitenant environment --- .../jobs/TenantAwareEqualsHashCodeAdvice.java | 61 ++++++++++ .../context/StepSynchronizationManager.java | 67 +++++++++++ .../AsyncLoanCOBExecutorServiceImpl.java | 2 +- .../jobs/service/JobRegisterServiceImpl.java | 2 +- .../jobs/service/JobStarter.java | 54 ++++++--- .../jobs/service/SchedulerJobListener.java | 106 +++++++++--------- .../service/SchedulerTriggerListener.java | 10 +- .../ClasspathDuplicatesStepDefinitions.java | 1 + .../jobs/service/JobStarterTest.java | 25 ++++- 9 files changed, 252 insertions(+), 76 deletions(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/jobs/TenantAwareEqualsHashCodeAdvice.java create mode 100644 fineract-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java 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 00000000000..9a3e295b5d0 --- /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/StepSynchronizationManager.java b/fineract-core/src/main/java/org/springframework/batch/core/scope/context/StepSynchronizationManager.java new file mode 100644 index 00000000000..ca9e995e92d --- /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 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 f4051256b61..544d0ace77d 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 @@ -106,7 +106,7 @@ private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate oldestCOBProce JobParameterDTO jobParameterCatchUpDTO = new JobParameterDTO(LoanCOBConstant.IS_CATCH_UP_PARAMETER_NAME, "true"); Set 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/jobs/service/JobRegisterServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/service/JobRegisterServiceImpl.java index b9dd7f26fcf..25fd77311e9 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 @@ private JobDetail createJobDetail(final ScheduledJobDetail scheduledJobDetail, S 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 031dc0737fd..a90fb902912 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.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.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> jobParameterProviders; private final JobNameService jobNameService; + private final TenantDetailsService tenantDetailsService; + private final AppUserRepositoryWrapper userRepository; + private final BusinessDateReadPlatformService businessDateReadPlatformService; public static final List FAILED_STATUSES = List.of(BatchStatus.FAILED, BatchStatus.ABANDONED, BatchStatus.STOPPED, BatchStatus.STOPPING, BatchStatus.UNKNOWN); - public JobExecution run(Job job, ScheduledJobDetail scheduledJobDetail, Set jobParameterDTOSet) - throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException, JobParametersInvalidException, - JobRestartException, JobExecutionException { - Map> 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 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 businessDates = businessDateReadPlatformService.getBusinessDates(); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); + ThreadLocalContextUtil.setBusinessDates(businessDates); + Map> 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> 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 6278eba1c57..00b169169e6 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 String getName() { } @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 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 a70fbcae991..ead4e9ead30 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 void triggerFired(Trigger trigger, JobExecutionContext context) { 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 5936c75940a..fc599797c24 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,7 @@ private boolean isHarmlessDuplicate(String resourcePath) { || 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/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 a9bec417171..4dea58338b8 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.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> jobParameterProviders; @Mock private JobNameService jobNameService; + + @Mock + private TenantDetailsService tenantDetailsService; + @Mock + private AppUserRepositoryWrapper userRepository; + @Mock + private BusinessDateReadPlatformService businessDateReadPlatformService; + @Captor private ArgumentCaptor> jobParameterDTOCaptor; @@ -106,7 +121,7 @@ public void runWithComplete() throws JobInstanceAlreadyCompleteException, JobExe 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 void runWithFailed() throws JobInstanceAlreadyCompleteException, JobExecu 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 @@ private void setupMocks(JobExecution jobExecution, Job job, ScheduledJobDetail s 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<>()); } }