FINERACT-2191: ThreadLocal context handling during job execution

This commit is contained in:
Oleksii Novikov
2025-03-27 11:44:12 +01:00
committed by Adam Saghy
parent 0f1d53cbdc
commit e584e26c39
20 changed files with 303 additions and 268 deletions
@@ -23,6 +23,7 @@ import java.time.LocalDate;
import java.util.HashMap;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.jackson.Jacksonized;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
@@ -31,6 +32,7 @@ import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
@Jacksonized
@Builder
@Getter
@EqualsAndHashCode
public class FineractContext implements Serializable {
private final String contextHolder;
@@ -38,4 +40,5 @@ public class FineractContext implements Serializable {
private final String authTokenContext;
private final HashMap<BusinessDateType, LocalDate> businessDateContext;
private final ActionContext actionContext;
}
@@ -20,10 +20,16 @@ package org.apache.fineract.infrastructure.core.domain;
import java.io.Serializable;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.jackson.Jacksonized;
@Jacksonized
@Builder
@EqualsAndHashCode
@RequiredArgsConstructor
@Getter
public class FineractPlatformTenant implements Serializable {
private final Long id;
@@ -32,32 +38,4 @@ public class FineractPlatformTenant implements Serializable {
private final String timezoneId;
private final FineractPlatformTenantConnection connection;
public FineractPlatformTenant(final Long id, final String tenantIdentifier, final String name, final String timezoneId,
final FineractPlatformTenantConnection connection) {
this.id = id;
this.tenantIdentifier = tenantIdentifier;
this.name = name;
this.timezoneId = timezoneId;
this.connection = connection;
}
public Long getId() {
return this.id;
}
public String getTenantIdentifier() {
return this.tenantIdentifier;
}
public String getName() {
return this.name;
}
public String getTimezoneId() {
return this.timezoneId;
}
public FineractPlatformTenantConnection getConnection() {
return connection;
}
}
@@ -27,7 +27,8 @@ import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.springframework.util.Assert;
/**
*
* A utility class for managing ThreadLocal context in the application. Provides methods for context initialization and
* cleanup.
*/
public final class ThreadLocalContextUtil {
@@ -124,4 +125,5 @@ public final class ThreadLocalContextUtil {
businessDateContext.remove();
actionContext.remove();
}
}
@@ -36,6 +36,7 @@ import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.NoExternalEvent;
import org.apache.fineract.infrastructure.event.external.service.ExternalEventService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionExecution;
import org.springframework.transaction.TransactionExecutionListener;
@@ -198,19 +199,29 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
}
@Override
public void beforeCommit(TransactionExecution transaction) {
List<BusinessEventWithContext> businessEventWithContexts = transactionBusinessEvents.get().peek();
if (!businessEventWithContexts.isEmpty()) {
FineractContext originalContext = ThreadLocalContextUtil.getContext();
try {
for (BusinessEventWithContext businessEventWithContext : businessEventWithContexts) {
ThreadLocalContextUtil.init(businessEventWithContext.getFineractContext());
externalEventService.postEvent(businessEventWithContext.getEvent());
}
} finally {
ThreadLocalContextUtil.init(originalContext);
}
public void beforeCommit(@NonNull final TransactionExecution transaction) {
final List<BusinessEventWithContext> businessEventWithContexts = transactionBusinessEvents.get().peek();
if (businessEventWithContexts.isEmpty()) {
return;
}
final FineractContext originalContext = ThreadLocalContextUtil.getContext();
businessEventWithContexts.forEach(businessEventWithContext -> {
final FineractContext currentContext = businessEventWithContext.getFineractContext();
boolean swappedContext = false;
try {
if (!originalContext.equals(currentContext)) {
swappedContext = true;
ThreadLocalContextUtil.init(currentContext);
}
externalEventService.postEvent(businessEventWithContext.getEvent());
} finally {
// Back to original context if we swapped it. We should restore the original context rather than reset
// it completely
if (swappedContext) {
ThreadLocalContextUtil.init(originalContext);
}
}
});
}
@Override
@@ -109,7 +109,7 @@ public class SendAsynchronousEventsTasklet implements Tasklet {
eventProducer.sendEvents(partitions);
}
private void markEventsAsSent(List<Long> eventIds) {
private void markEventsAsSent(final List<Long> eventIds) {
OffsetDateTime sentAt = DateUtils.getAuditOffsetDateTime();
// Partitioning dataset to avoid exception: PreparedStatement can have at most 65,535 parameters
@@ -117,20 +117,23 @@ public class SendAsynchronousEventsTasklet implements Tasklet {
List<List<Long>> partitions = Lists.partition(eventIds, partitionSize);
List<Future<?>> tasks = new ArrayList<>();
final FineractContext context = ThreadLocalContextUtil.getContext();
partitions //
.forEach(partitionedEventIds -> {
tasks.add(threadPoolTaskExecutor.submit(() -> {
ThreadLocalContextUtil.init(context);
transactionTemplate.execute((status) -> {
measure(() -> {
repository.markEventsSent(partitionedEventIds, sentAt);
}, timeTaken -> {
log.debug("Took {}ms to update {} events", timeTaken.toMillis(), partitionedEventIds.size());
});
return null;
partitions.forEach(partitionedEventIds -> {
tasks.add(threadPoolTaskExecutor.submit(() -> {
try {
ThreadLocalContextUtil.init(context);
transactionTemplate.execute((status) -> {
measure(() -> {
repository.markEventsSent(partitionedEventIds, sentAt);
}, timeTaken -> {
log.debug("Took {}ms to update {} events", timeTaken.toMillis(), partitionedEventIds.size());
});
}));
});
return null;
});
} finally {
ThreadLocalContextUtil.reset();
}
}));
});
for (Future<?> task : tasks) {
try {
task.get();
@@ -41,9 +41,12 @@ public class RecalculateInterestPoster implements Callable<Void> {
@Override
public Void call() throws JobExecutionException {
ThreadLocalContextUtil.init(fineractContext);
if (!loanIds.isEmpty()) {
List<Throwable> errors = new ArrayList<>();
if (loanIds.isEmpty()) {
return null;
}
try {
ThreadLocalContextUtil.init(fineractContext);
final List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) {
log.debug("Loan ID {}", loanId);
try {
@@ -55,6 +58,8 @@ public class RecalculateInterestPoster implements Callable<Void> {
if (!errors.isEmpty()) {
throw new JobExecutionException(errors);
}
} finally {
ThreadLocalContextUtil.reset();
}
return null;
}
@@ -38,6 +38,9 @@ import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* Tasklet to initialize the thread local context for job execution
*/
@Slf4j
@RequiredArgsConstructor
public class InitialisationTasklet implements Tasklet {
@@ -51,14 +54,17 @@ public class InitialisationTasklet implements Tasklet {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
ThreadLocalContextUtil.setActionContext(ActionContext.COB);
String businessDateString = Objects.requireNonNull((String) chunkContext.getStepContext().getStepExecution().getJobExecution()
.getExecutionContext().get(LoanCOBConstant.BUSINESS_DATE_PARAMETER_NAME));
LocalDate businessDate = LocalDate.parse(businessDateString, DateTimeFormatter.ISO_DATE);
businessDates.put(BusinessDateType.COB_DATE, businessDate);
businessDates.put(BusinessDateType.BUSINESS_DATE, businessDate.plusDays(1));
ThreadLocalContextUtil.setBusinessDates(businessDates);
log.debug("Initialisation with Business Date [{}], COB Date [{}] and Action Context [{}]", businessDate.plusDays(1), businessDate,
ThreadLocalContextUtil.getActionContext());
log.debug("Initialized context with Business Date [{}], COB Date [{}] and Action Context [{}]", businessDate.plusDays(1),
businessDate, ThreadLocalContextUtil.getActionContext());
return RepeatStatus.FINISHED;
}
}
@@ -18,19 +18,30 @@
*/
package org.apache.fineract.cob.loan;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.FineractContext;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.task.TaskDecorator;
import org.springframework.lang.NonNull;
/**
* Task decorator to ensure proper thread context propagation and cleanup
*/
@Slf4j
public class ContextAwareTaskDecorator implements TaskDecorator {
@NonNull
@Override
public Runnable decorate(@NotNull Runnable runnable) {
public Runnable decorate(@NonNull final Runnable runnable) {
final FineractContext context = ThreadLocalContextUtil.getContext();
return () -> {
ThreadLocalContextUtil.init(context);
runnable.run();
try {
log.debug("Initializing thread context for decorated task");
ThreadLocalContextUtil.init(context);
runnable.run();
} finally {
ThreadLocalContextUtil.reset();
}
};
}
@@ -18,7 +18,6 @@
*/
package org.apache.fineract.cob.service;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
@@ -37,11 +36,11 @@ import org.apache.fineract.infrastructure.core.domain.FineractContext;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
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.domain.ScheduledJobDetailRepository;
import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException;
import org.apache.fineract.infrastructure.jobs.service.JobStarter;
import org.apache.fineract.infrastructure.jobs.service.SchedulerServiceConstants;
import org.quartz.JobExecutionException;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParametersInvalidException;
@@ -63,12 +62,10 @@ public class AsyncLoanCOBExecutorServiceImpl implements AsyncLoanCOBExecutorServ
private final JobLocator jobLocator;
private final ScheduledJobDetailRepository scheduledJobDetailRepository;
private final JobStarter jobStarter;
private final JobParameterRepository jobParameterRepository;
private final RetrieveLoanIdService retrieveLoanIdService;
@Override
@Async(TaskExecutorConstant.LOAN_COB_CATCH_UP_TASK_EXECUTOR_BEAN_NAME)
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
public void executeLoanCOBCatchUpAsync(FineractContext context) {
try {
ThreadLocalContextUtil.init(context);
@@ -80,35 +77,36 @@ public class AsyncLoanCOBExecutorServiceImpl implements AsyncLoanCOBExecutorServ
? loanIdAndLastClosedBusinessDate.get(0).getLastClosedBusinessDate()
: cobBusinessDate;
if (DateUtils.isBefore(oldestCOBProcessedDate, cobBusinessDate)) {
executeLoanCOBDayByDayUntilCOBBusinessDate(oldestCOBProcessedDate, cobBusinessDate, context);
executeLoanCOBDayByDayUntilCOBBusinessDate(oldestCOBProcessedDate, cobBusinessDate);
}
} catch (NoSuchJobException e) {
// Throwing an error here is useless as it will be swallowed hence it is async method
log.error("", new JobNotFoundException(LoanCOBConstant.JOB_NAME, e));
log.error("Job not found: {}", LoanCOBConstant.JOB_NAME, new JobNotFoundException(LoanCOBConstant.JOB_NAME, e));
} catch (JobInstanceAlreadyCompleteException | JobRestartException | JobParametersInvalidException
| JobExecutionAlreadyRunningException | JobExecutionException e) {
// Throwing an error here is useless as it will be swallowed hence it is async method
log.error("", e);
log.error("Error executing job", e);
} finally {
ThreadLocalContextUtil.reset();
}
}
private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate oldestCOBProcessedDate, LocalDate cobBusinessDate,
FineractContext context) throws NoSuchJobException, JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException,
private void executeLoanCOBDayByDayUntilCOBBusinessDate(LocalDate oldestCOBProcessedDate, LocalDate cobBusinessDate)
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);
String tenantIdentifier = ThreadLocalContextUtil.getTenant().getTenantIdentifier();
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");
JobParameterDTO tenantParameterDTO = new JobParameterDTO(SchedulerServiceConstants.TENANT_IDENTIFIER, tenantIdentifier);
Set<JobParameterDTO> jobParameters = new HashSet<>();
Collections.addAll(jobParameters, jobParameterDTO, jobParameterCatchUpDTO);
jobStarter.run(job, scheduledJobDetail, jobParameters, ThreadLocalContextUtil.getTenant().getTenantIdentifier());
Collections.addAll(jobParameters, jobParameterDTO, jobParameterCatchUpDTO, tenantParameterDTO);
jobStarter.run(job, scheduledJobDetail, jobParameters, tenantIdentifier);
executingBusinessDate = executingBusinessDate.plusDays(1);
}
}
@@ -24,6 +24,8 @@ import java.io.IOException;
import java.net.URLConnection;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.bulkimport.data.BulkImportEvent;
import org.apache.fineract.infrastructure.bulkimport.data.Count;
import org.apache.fineract.infrastructure.bulkimport.data.GlobalEntityType;
@@ -33,134 +35,88 @@ import org.apache.fineract.infrastructure.bulkimport.importhandler.ImportHandler
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
import org.apache.fineract.infrastructure.documentmanagement.command.DocumentCommand;
import org.apache.fineract.infrastructure.documentmanagement.domain.Document;
import org.apache.fineract.infrastructure.documentmanagement.service.DocumentWritePlatformService;
import org.apache.poi.ss.usermodel.Workbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class BulkImportEventListener implements ApplicationListener<BulkImportEvent> {
private static final Logger LOG = LoggerFactory.getLogger(BulkImportEventListener.class);
private final TenantDetailsService tenantDetailsService;
private final ApplicationContext applicationContext;
private final ImportDocumentRepository importRepository;
private final DocumentWritePlatformService documentService;
@Autowired
public BulkImportEventListener(final TenantDetailsService tenantDetailsService, final ApplicationContext context,
final ImportDocumentRepository importRepository, final DocumentWritePlatformService documentService) {
this.tenantDetailsService = tenantDetailsService;
this.applicationContext = context;
this.importRepository = importRepository;
this.documentService = documentService;
}
@Override
public void onApplicationEvent(final BulkImportEvent event) {
ThreadLocalContextUtil.init(event.getContext());
ImportHandler importHandler = null;
final ImportDocument importDocument = this.importRepository.findById(event.getImportId()).orElse(null);
final GlobalEntityType entityType = GlobalEntityType.fromInt(importDocument.getEntityType());
switch (entityType) {
case OFFICES:
importHandler = this.applicationContext.getBean("officeImportHandler", ImportHandler.class);
break;
case CENTERS:
importHandler = this.applicationContext.getBean("centerImportHandler", ImportHandler.class);
break;
case CHART_OF_ACCOUNTS:
importHandler = this.applicationContext.getBean("chartOfAccountsImportHandler", ImportHandler.class);
break;
case CLIENTS_ENTITY:
importHandler = this.applicationContext.getBean("clientEntityImportHandler", ImportHandler.class);
break;
case CLIENTS_PERSON:
importHandler = this.applicationContext.getBean("clientPersonImportHandler", ImportHandler.class);
break;
case FIXED_DEPOSIT_ACCOUNTS:
importHandler = this.applicationContext.getBean("fixedDepositImportHandler", ImportHandler.class);
break;
case FIXED_DEPOSIT_TRANSACTIONS:
importHandler = this.applicationContext.getBean("fixedDepositTransactionImportHandler", ImportHandler.class);
break;
case GROUPS:
importHandler = this.applicationContext.getBean("groupImportHandler", ImportHandler.class);
break;
case GUARANTORS:
importHandler = this.applicationContext.getBean("guarantorImportHandler", ImportHandler.class);
break;
case GL_JOURNAL_ENTRIES:
importHandler = this.applicationContext.getBean("journalEntriesImportHandler", ImportHandler.class);
break;
case LOANS:
importHandler = this.applicationContext.getBean("loanImportHandler", ImportHandler.class);
break;
case LOAN_TRANSACTIONS:
importHandler = this.applicationContext.getBean("loanRepaymentImportHandler", ImportHandler.class);
break;
case RECURRING_DEPOSIT_ACCOUNTS:
importHandler = this.applicationContext.getBean("recurringDepositImportHandler", ImportHandler.class);
break;
case RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS:
importHandler = this.applicationContext.getBean("recurringDepositTransactionImportHandler", ImportHandler.class);
break;
case SAVINGS_ACCOUNT:
importHandler = this.applicationContext.getBean("savingsImportHandler", ImportHandler.class);
break;
case SAVINGS_TRANSACTIONS:
importHandler = this.applicationContext.getBean("savingsTransactionImportHandler", ImportHandler.class);
break;
case SHARE_ACCOUNTS:
importHandler = this.applicationContext.getBean("sharedAccountImportHandler", ImportHandler.class);
break;
case STAFF:
importHandler = this.applicationContext.getBean("staffImportHandler", ImportHandler.class);
break;
case USERS:
importHandler = this.applicationContext.getBean("userImportHandler", ImportHandler.class);
break;
default:
throw new GeneralPlatformDomainRuleException("error.msg.unable.to.find.resource", "Unable to find requested resource");
}
final Workbook workbook = event.getWorkbook();
final Count count = importHandler.process(workbook, event.getLocale(), event.getDateFormat());
importDocument.update(DateUtils.getLocalDateTimeOfTenant(), count.getSuccessCount(), count.getErrorCount());
this.importRepository.saveAndFlush(importDocument);
final Set<String> modifiedParams = new HashSet<>();
modifiedParams.add("fileName");
modifiedParams.add("size");
modifiedParams.add("type");
modifiedParams.add("location");
Document document = importDocument.getDocument();
DocumentCommand documentCommand = new DocumentCommand(modifiedParams, document.getId(), entityType.name(), null, document.getName(),
document.getFileName(), document.getSize(), URLConnection.guessContentTypeFromName(document.getFileName()), null, null);
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ThreadLocalContextUtil.init(event.getContext());
final ImportDocument importDocument = this.importRepository.findById(event.getImportId()).orElse(null);
final GlobalEntityType entityType = GlobalEntityType.fromInt(importDocument.getEntityType());
final ImportHandler importHandler = switch (entityType) {
case OFFICES -> this.applicationContext.getBean("officeImportHandler", ImportHandler.class);
case CENTERS -> this.applicationContext.getBean("centerImportHandler", ImportHandler.class);
case CHART_OF_ACCOUNTS -> this.applicationContext.getBean("chartOfAccountsImportHandler", ImportHandler.class);
case CLIENTS_ENTITY -> this.applicationContext.getBean("clientEntityImportHandler", ImportHandler.class);
case CLIENTS_PERSON -> this.applicationContext.getBean("clientPersonImportHandler", ImportHandler.class);
case FIXED_DEPOSIT_ACCOUNTS -> this.applicationContext.getBean("fixedDepositImportHandler", ImportHandler.class);
case FIXED_DEPOSIT_TRANSACTIONS ->
this.applicationContext.getBean("fixedDepositTransactionImportHandler", ImportHandler.class);
case GROUPS -> this.applicationContext.getBean("groupImportHandler", ImportHandler.class);
case GUARANTORS -> this.applicationContext.getBean("guarantorImportHandler", ImportHandler.class);
case GL_JOURNAL_ENTRIES -> this.applicationContext.getBean("journalEntriesImportHandler", ImportHandler.class);
case LOANS -> this.applicationContext.getBean("loanImportHandler", ImportHandler.class);
case LOAN_TRANSACTIONS -> this.applicationContext.getBean("loanRepaymentImportHandler", ImportHandler.class);
case RECURRING_DEPOSIT_ACCOUNTS -> this.applicationContext.getBean("recurringDepositImportHandler", ImportHandler.class);
case RECURRING_DEPOSIT_ACCOUNTS_TRANSACTIONS ->
this.applicationContext.getBean("recurringDepositTransactionImportHandler", ImportHandler.class);
case SAVINGS_ACCOUNT -> this.applicationContext.getBean("savingsImportHandler", ImportHandler.class);
case SAVINGS_TRANSACTIONS -> this.applicationContext.getBean("savingsTransactionImportHandler", ImportHandler.class);
case SHARE_ACCOUNTS -> this.applicationContext.getBean("sharedAccountImportHandler", ImportHandler.class);
case STAFF -> this.applicationContext.getBean("staffImportHandler", ImportHandler.class);
case USERS -> this.applicationContext.getBean("userImportHandler", ImportHandler.class);
default ->
throw new GeneralPlatformDomainRuleException("error.msg.unable.to.find.resource", "Unable to find requested resource");
};
final Workbook workbook = event.getWorkbook();
final Count count = importHandler.process(workbook, event.getLocale(), event.getDateFormat());
importDocument.update(DateUtils.getLocalDateTimeOfTenant(), count.getSuccessCount(), count.getErrorCount());
this.importRepository.saveAndFlush(importDocument);
final Set<String> modifiedParams = new HashSet<>();
modifiedParams.add("fileName");
modifiedParams.add("size");
modifiedParams.add("type");
modifiedParams.add("location");
Document document = importDocument.getDocument();
DocumentCommand documentCommand = new DocumentCommand(modifiedParams, document.getId(), entityType.name(), null,
document.getName(), document.getFileName(), document.getSize(),
URLConnection.guessContentTypeFromName(document.getFileName()), null, null);
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
workbook.write(bos);
} finally {
bos.close();
try {
workbook.write(bos);
} finally {
bos.close();
}
} catch (IOException io) {
log.error("Problem occurred in onApplicationEvent function", io);
}
} catch (IOException io) {
LOG.error("Problem occurred in onApplicationEvent function", io);
byte[] bytes = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
this.documentService.updateDocument(documentCommand, bis);
} finally {
ThreadLocalContextUtil.reset();
}
byte[] bytes = bos.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
this.documentService.updateDocument(documentCommand, bis);
}
}
@@ -42,28 +42,33 @@ public class FineractHookListener implements HookListener {
@Override
public void onApplicationEvent(final HookEvent event) {
ThreadLocalContextUtil.init(event.getContext());
try {
ThreadLocalContextUtil.init(event.getContext());
final AppUser appUser = event.getAppUser();
final AppUser appUser = event.getAppUser();
final HookEventSource hookEventSource = (HookEventSource) event.getSource();
final FineractContext fineractContext = event.getContext();
final String entityName = hookEventSource.getEntityName();
final String actionName = hookEventSource.getActionName();
final String payload = event.getPayload();
final HookEventSource hookEventSource = (HookEventSource) event.getSource();
final FineractContext fineractContext = event.getContext();
final String entityName = hookEventSource.getEntityName();
final String actionName = hookEventSource.getActionName();
final String payload = event.getPayload();
final List<Hook> hooks = hookReadPlatformService.retrieveHooksByEvent(hookEventSource.getEntityName(),
hookEventSource.getActionName());
final List<Hook> hooks = hookReadPlatformService.retrieveHooksByEvent(hookEventSource.getEntityName(),
hookEventSource.getActionName());
for (final Hook hook : hooks) {
final HookProcessor processor = hookProcessorProvider.getProcessor(hook);
try {
processor.process(hook, payload, entityName, actionName, fineractContext);
} catch (Throwable e) {
log.error("Hook {} failed in HookProcessor {} for tenantIdentifier/user {}/{}, entityName: {}, actionName: {}, payload {} ",
hook.getId(), processor.getClass().getSimpleName(), fineractContext.getTenantContext().getTenantIdentifier(),
appUser.getDisplayName(), entityName, actionName, payload, e);
for (final Hook hook : hooks) {
final HookProcessor processor = hookProcessorProvider.getProcessor(hook);
try {
processor.process(hook, payload, entityName, actionName, fineractContext);
} catch (Throwable e) {
log.error(
"Hook {} failed in HookProcessor {} for tenantIdentifier/user {}/{}, entityName: {}, actionName: {}, payload {} ",
hook.getId(), processor.getClass().getSimpleName(), fineractContext.getTenantContext().getTenantIdentifier(),
appUser.getDisplayName(), entityName, actionName, payload, e);
}
}
} finally {
ThreadLocalContextUtil.reset();
}
}
}
@@ -78,16 +78,24 @@ public class JobStarter {
public JobExecution run(Job job, ScheduledJobDetail scheduledJobDetail, Set<JobParameterDTO> jobParameterDTOSet,
String tenantIdentifier) throws JobInstanceAlreadyCompleteException, JobExecutionAlreadyRunningException,
JobParametersInvalidException, JobRestartException, JobExecutionException {
boolean contextInitialized = false;
final FineractPlatformTenant existingTenant = ThreadLocalContextUtil.getTenant();
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);
if (existingTenant == null) {
contextInitialized = true;
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))
@@ -101,7 +109,9 @@ public class JobStarter {
}
return result;
} finally {
ThreadLocalContextUtil.reset();
if (contextInitialized) {
ThreadLocalContextUtil.reset();
}
}
}
@@ -20,13 +20,11 @@ package org.apache.fineract.infrastructure.jobs.service;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadPlatformService;
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.AppUserRepositoryWrapper;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
@@ -43,8 +41,6 @@ import org.springframework.stereotype.Component;
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;
@@ -61,10 +57,17 @@ public class SchedulerJobListener implements JobListener {
@Override
public void jobWasExecuted(final JobExecutionContext context, final JobExecutionException jobException) {
final String tenantIdentifier = context.getMergedJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
final FineractPlatformTenant existingTenant = ThreadLocalContextUtil.getTenant();
boolean contextInitialized = false;
try {
String tenantIdentifier = context.getMergedJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
FineractPlatformTenant tenant = tenantDetailsService.loadTenantById(tenantIdentifier);
ThreadLocalContextUtil.setTenant(tenant);
if (existingTenant == null || !existingTenant.getTenantIdentifier().equals(tenantIdentifier)) {
contextInitialized = true;
final FineractPlatformTenant tenant = tenantDetailsService.loadTenantById(tenantIdentifier);
ThreadLocalContextUtil.setTenant(tenant);
}
final Trigger trigger = context.getTrigger();
final JobKey key = context.getJobDetail().getKey();
@@ -105,11 +108,12 @@ public class SchedulerJobListener implements JobListener {
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();
if (contextInitialized) {
ThreadLocalContextUtil.reset();
}
}
}
@@ -50,14 +50,22 @@ public class SchedulerTriggerListener implements TriggerListener {
@Override
public boolean vetoJobExecution(final Trigger trigger, final JobExecutionContext context) {
String tenantIdentifier = trigger.getJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
FineractPlatformTenant tenant = tenantDetailsService.loadTenantById(tenantIdentifier);
final String tenantIdentifier = trigger.getJobDataMap().getString(SchedulerServiceConstants.TENANT_IDENTIFIER);
final FineractPlatformTenant existingTenant = ThreadLocalContextUtil.getTenant();
boolean contextInitialized = false;
try {
ThreadLocalContextUtil.setTenant(tenant);
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
if (existingTenant == null || !existingTenant.getTenantIdentifier().equals(tenantIdentifier)) {
contextInitialized = true;
final FineractPlatformTenant tenant = tenantDetailsService.loadTenantById(tenantIdentifier);
ThreadLocalContextUtil.setTenant(tenant);
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
}
return schedulerVetoer.veto(trigger, context);
} finally {
ThreadLocalContextUtil.reset();
if (contextInitialized) {
ThreadLocalContextUtil.reset();
}
}
}
@@ -25,7 +25,6 @@ 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.JdbcTemplateFactory;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.core.service.tenant.TenantDetailsService;
@@ -37,6 +36,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@@ -55,30 +55,32 @@ public class StuckJobListener implements ApplicationListener<ContextRefreshedEve
private final AppUserRepositoryWrapper userRepository;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!jobRegistry.getJobNames().isEmpty()) {
List<FineractPlatformTenant> allTenants = tenantDetailsService.findAllTenants();
allTenants.forEach(tenant -> {
public void onApplicationEvent(@NonNull final ContextRefreshedEvent event) {
if (jobRegistry.getJobNames().isEmpty()) {
return;
}
tenantDetailsService.findAllTenants().forEach(tenant -> {
try {
ThreadLocalContextUtil.setTenant(tenant);
NamedParameterJdbcTemplate namedParameterJdbcTemplate = jdbcTemplateFactory.createNamedParameterJdbcTemplate(tenant);
List<String> stuckJobNames = jobExecutionRepository.getStuckJobNames(namedParameterJdbcTemplate);
final NamedParameterJdbcTemplate namedParameterJdbcTemplate = jdbcTemplateFactory.createNamedParameterJdbcTemplate(tenant);
final List<String> stuckJobNames = jobExecutionRepository.getStuckJobNames(namedParameterJdbcTemplate);
if (!stuckJobNames.isEmpty()) {
try {
HashMap<BusinessDateType, LocalDate> businessDates = businessDateReadPlatformService.getBusinessDates();
final HashMap<BusinessDateType, LocalDate> businessDates = businessDateReadPlatformService.getBusinessDates();
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil.setBusinessDates(businessDates);
AppUser user = userRepository.fetchSystemUser();
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, user.getPassword(),
final AppUser user = userRepository.fetchSystemUser();
final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, user.getPassword(),
user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
stuckJobNames.forEach(stuckJobExecutorService::resumeStuckJob);
} catch (Exception e) {
throw new RuntimeException("Error while trying to restart stuck jobs", e);
} finally {
ThreadLocalContextUtil.reset();
SecurityContextHolder.getContext().setAuthentication(null);
}
}
});
}
} finally {
ThreadLocalContextUtil.reset();
}
});
}
}
@@ -155,8 +155,12 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob
@Override
public void run() {
ThreadLocalContextUtil.init(context);
connectAndSendToIntermediateServer(apiQueueResourceDatas);
try {
ThreadLocalContextUtil.init(context);
connectAndSendToIntermediateServer(apiQueueResourceDatas);
} finally {
ThreadLocalContextUtil.reset();
}
}
@Override
@@ -18,29 +18,43 @@
*/
package org.apache.fineract.infrastructure.springbatch;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.jetbrains.annotations.NotNull;
import org.springframework.batch.integration.partition.StepExecutionRequest;
import org.springframework.lang.NonNull;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.support.ExecutorChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
/**
* Channel interceptor for Spring Batch message handling that ensures ThreadLocal context is properly initialized before
* message handling and cleaned up afterwards
*/
@Slf4j
public class InputChannelInterceptor implements ExecutorChannelInterceptor {
@Override
public Message<StepExecutionRequest> beforeHandle(Message<?> message, @NotNull MessageChannel channel,
@NotNull MessageHandler handler) {
public Message<StepExecutionRequest> beforeHandle(@NonNull final Message<?> message, @NonNull final MessageChannel channel,
@NonNull final MessageHandler handler) {
return beforeHandleMessage(message);
}
@Override
public void afterMessageHandled(@NonNull final Message<?> message, @NonNull final MessageChannel channel,
@NonNull final MessageHandler handler, final Exception ex) {
log.debug("Cleaning up ThreadLocal context after message handling");
ThreadLocalContextUtil.reset();
}
public Message<StepExecutionRequest> beforeHandleMessage(Message<?> message) {
return new GenericMessage<>(beforeHandleMessage((ContextualMessage) message.getPayload()));
}
public StepExecutionRequest beforeHandleMessage(ContextualMessage contextualMessage) {
log.debug("Initializing ThreadLocal context for message handling");
ThreadLocalContextUtil.init(contextualMessage.getContext());
ThreadLocalContextUtil.setActionContext(ActionContext.COB);
return contextualMessage.getStepExecutionRequest();
@@ -24,6 +24,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.notification.data.NotificationData;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Profile;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
@Component
@@ -35,10 +36,15 @@ public class SpringNotificationEventListener implements ApplicationListener<Noti
private final NotificationEventListener notificationEventListener;
@Override
public void onApplicationEvent(NotificationEvent event) {
public void onApplicationEvent(@NonNull final NotificationEvent event) {
log.debug("Processing Spring notification event {}", event);
ThreadLocalContextUtil.init(event.getContext());
NotificationData notificationData = event.getNotificationData();
notificationEventListener.receive(notificationData);
try {
ThreadLocalContextUtil.init(event.getContext());
final NotificationData notificationData = event.getNotificationData();
notificationEventListener.receive(notificationData);
} finally {
ThreadLocalContextUtil.reset();
}
}
}
@@ -117,23 +117,27 @@ public class PostInterestForSavingTasklet implements Tasklet {
FineractContext context = ThreadLocalContextUtil.getContext();
Callable<Void> fetchData = () -> {
ThreadLocalContextUtil.init(context);
Long maxId = maxSavingsIdInList;
if (!queue.isEmpty()) {
maxId = Math.max(maxSavingsIdInList, queue.element().get(queue.element().size() - 1).getId());
}
while (queue.size() <= QUEUE_SIZE) {
log.debug("Fetching while threads are running!");
List<SavingsAccountData> savingsAccountDataList = Collections.synchronizedList(this.savingAccountReadPlatformService
.retrieveAllSavingsDataForInterestPosting(backdatedTxnsAllowedTill, pageSize, ACTIVE.getValue(), maxId));
if (savingsAccountDataList.isEmpty()) {
break;
try {
ThreadLocalContextUtil.init(context);
Long maxId = maxSavingsIdInList;
if (!queue.isEmpty()) {
maxId = Math.max(maxSavingsIdInList, queue.element().get(queue.element().size() - 1).getId());
}
maxId = savingsAccountDataList.get(savingsAccountDataList.size() - 1).getId();
queue.add(savingsAccountDataList);
while (queue.size() <= QUEUE_SIZE) {
log.debug("Fetching while threads are running!");
List<SavingsAccountData> savingsAccountDataList = Collections.synchronizedList(this.savingAccountReadPlatformService
.retrieveAllSavingsDataForInterestPosting(backdatedTxnsAllowedTill, pageSize, ACTIVE.getValue(), maxId));
if (savingsAccountDataList.isEmpty()) {
break;
}
maxId = savingsAccountDataList.get(savingsAccountDataList.size() - 1).getId();
queue.add(savingsAccountDataList);
}
return null;
} finally {
ThreadLocalContextUtil.reset();
}
return null;
};
posters.add(fetchData);
@@ -25,6 +25,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.FineractContext;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.savings.data.SavingsAccountData;
/**
@@ -40,10 +41,14 @@ public class SavingsSchedularInterestPosterTask implements Callable<Void> {
private FineractContext context;
@Override
public Void call() throws org.apache.fineract.infrastructure.jobs.exception.JobExecutionException {
ThreadLocalContextUtil.init(context);
interestPoster.postInterest();
return null;
public Void call() throws JobExecutionException {
try {
ThreadLocalContextUtil.init(context);
interestPoster.postInterest();
return null;
} finally {
ThreadLocalContextUtil.reset();
}
}
public void setSavingAccounts(Collection<SavingsAccountData> savingAccounts) {