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();