This is an automated email from the ASF dual-hosted git repository.
adamsaghy 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 141e26fda FINERACT-1694: Upgrade External Business Event Handling
141e26fda is described below
commit 141e26fda7abafabd2eff5e9d23f49c2030990ec
Author: Soma Sörös <[email protected]>
AuthorDate: Wed Jul 31 09:33:15 2024 +0200
FINERACT-1694: Upgrade External Business Event Handling
---
.../service/BusinessEventNotifierServiceImpl.java | 74 ++++++++++-
.../event/business/service/TransactionHelper.java | 33 +++++
.../BusinessEventNotifierServiceImplTest.java | 137 +++++++++++++++++++++
.../CustomSnapshotEventIntegrationTest.java | 18 ++-
4 files changed, 255 insertions(+), 7 deletions(-)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
index 9809f6342..4543539d1 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImpl.java
@@ -22,9 +22,14 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Stack;
+import lombok.Getter;
import lombok.RequiredArgsConstructor;
+import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.domain.FineractContext;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
import
org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
@@ -33,12 +38,16 @@ import
org.apache.fineract.infrastructure.event.external.repository.ExternalEven
import
org.apache.fineract.infrastructure.event.external.service.ExternalEventService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.TransactionExecution;
+import org.springframework.transaction.TransactionExecutionListener;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
@Service
@SuppressWarnings({ "unchecked", "rawtypes" })
@RequiredArgsConstructor
@Slf4j
-public class BusinessEventNotifierServiceImpl implements
BusinessEventNotifierService, InitializingBean {
+public class BusinessEventNotifierServiceImpl implements
BusinessEventNotifierService, InitializingBean, TransactionExecutionListener {
private final Map<Class, List<BusinessEventListener>> preListeners = new
HashMap<>();
private final Map<Class, List<BusinessEventListener>> postListeners = new
HashMap<>();
@@ -49,6 +58,8 @@ public class BusinessEventNotifierServiceImpl implements
BusinessEventNotifierSe
private final ExternalEventService externalEventService;
private final ExternalEventConfigurationRepository
eventConfigurationRepository;
private final FineractProperties fineractProperties;
+ private final ThreadLocal<Stack<List<BusinessEventWithContext>>>
transactionBusinessEvents = ThreadLocal.withInitial(Stack::new);
+ private final TransactionHelper transactionHelper;
@Override
public void afterPropertiesSet() throws Exception {
@@ -79,6 +90,7 @@ public class BusinessEventNotifierServiceImpl implements
BusinessEventNotifierSe
}
@Override
+ @Transactional(propagation = Propagation.SUPPORTS)
public void notifyPostBusinessEvent(BusinessEvent<?> businessEvent) {
throwExceptionIfBulkEvent(businessEvent);
boolean isExternalEvent = !(businessEvent instanceof NoExternalEvent);
@@ -92,7 +104,11 @@ public class BusinessEventNotifierServiceImpl implements
BusinessEventNotifierSe
if (isExternalEventRecordingEnabled()) {
recordedEvents.get().add(businessEvent);
} else {
- externalEventService.postEvent(businessEvent);
+ if (transactionHelper.hasTransaction()) {
+ storeTransactionalBusinessEvent(businessEvent);
+ } else {
+ externalEventService.postEvent(businessEvent);
+ }
}
}
}
@@ -170,4 +186,58 @@ public class BusinessEventNotifierServiceImpl implements
BusinessEventNotifierSe
eventRecordingEnabled.set(false);
recordedEvents.remove();
}
+
+ private void storeTransactionalBusinessEvent(BusinessEvent<?>
businessEvent) {
+ List<BusinessEventWithContext> businessEvents =
transactionBusinessEvents.get().peek();
+ FineractContext fineractContext = ThreadLocalContextUtil.getContext();
+ businessEvents.add(new BusinessEventWithContext(businessEvent,
fineractContext));
+ }
+
+ private void cleanup() {
+ transactionBusinessEvents.get().pop();
+ }
+
+ @Override
+ public void afterBegin(TransactionExecution transaction, Throwable
beginFailure) {
+ transactionBusinessEvents.get().push(new ArrayList<>());
+ }
+
+ @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);
+ }
+ }
+ }
+
+ @Override
+ public void afterCommit(TransactionExecution transaction, Throwable
commitFailure) {
+ cleanup();
+ }
+
+ @Override
+ public void afterRollback(TransactionExecution transaction, Throwable
rollbackFailure) {
+ cleanup();
+ }
+
+ @Getter
+ @Setter
+ private static final class BusinessEventWithContext {
+
+ private BusinessEvent<?> event;
+ private FineractContext fineractContext;
+
+ BusinessEventWithContext(BusinessEvent<?> event, FineractContext
fineractContext) {
+ this.event = event;
+ this.fineractContext = fineractContext;
+ }
+ }
}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/TransactionHelper.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/TransactionHelper.java
new file mode 100644
index 000000000..7a30304e0
--- /dev/null
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/business/service/TransactionHelper.java
@@ -0,0 +1,33 @@
+/**
+ * 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.event.business.service;
+
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.interceptor.TransactionAspectSupport;
+
+@Component
+@Transactional(propagation = Propagation.SUPPORTS)
+public class TransactionHelper {
+
+ boolean hasTransaction() {
+ return
TransactionAspectSupport.currentTransactionStatus().hasTransaction();
+ }
+}
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
index f69be4296..73d0ee2b9 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/business/service/BusinessEventNotifierServiceImplTest.java
@@ -27,7 +27,11 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
+import java.time.LocalDate;
+import java.util.HashMap;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
import
org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
@@ -43,6 +47,7 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
+import org.springframework.transaction.TransactionExecution;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -58,9 +63,140 @@ class BusinessEventNotifierServiceImplTest {
@Mock
private FineractProperties fineractProperties;
+ @Mock
+ private TransactionHelper transactionHelper;
+
@InjectMocks
private BusinessEventNotifierServiceImpl underTest;
+ @Test
+ public void
testNotifyPostBusinessEventShouldCollectEventsWithinTransaction() {
+ // given
+ setBusinessDate();
+ configureExternalEventsProperties(true);
+ MockBusinessEvent event = new MockBusinessEvent();
+ BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+ underTest.addPostBusinessEventListener(MockBusinessEvent.class,
postListener);
+ TransactionExecution mockTransaction =
mock(TransactionExecution.class);
+ underTest.afterBegin(mockTransaction, null);
+ // when
+
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+ .thenReturn(new ExternalEventConfiguration("aType", true));
+ when(transactionHelper.hasTransaction()).thenReturn(true);
+ underTest.notifyPostBusinessEvent(event);
+ // then
+ verify(postListener).onBusinessEvent(event);
+ verifyNoInteractions(externalEventService);
+ // simulate finish transaction
+ underTest.beforeCommit(mockTransaction);
+ verify(externalEventService).postEvent(event);
+ underTest.afterCommit(mockTransaction, null);
+ verifyNoInteractions(mockTransaction);
+ }
+
+ private void setBusinessDate() {
+ HashMap<BusinessDateType, LocalDate> map = new HashMap<>(2);
+ map.put(BusinessDateType.BUSINESS_DATE, LocalDate.of(2023, 2, 1));
+ map.put(BusinessDateType.COB_DATE, LocalDate.of(2023, 1, 31));
+ ThreadLocalContextUtil.setBusinessDates(map);
+ }
+
+ @Test
+ public void
testNotifyPostBusinessEventShouldCollectEventsWithinTransactionInNestedTransaction()
{
+ // given
+ setBusinessDate();
+ configureExternalEventsProperties(true);
+ MockBusinessEvent event = new MockBusinessEvent();
+ MockBusinessEvent nestedEvent = new MockBusinessEvent();
+ BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+ underTest.addPostBusinessEventListener(MockBusinessEvent.class,
postListener);
+ TransactionExecution mockTransaction =
mock(TransactionExecution.class);
+ // when
+
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+ .thenReturn(new ExternalEventConfiguration("aType", true));
+ when(transactionHelper.hasTransaction()).thenReturn(true);
+
+ // simulate outer transaction
+ underTest.afterBegin(mockTransaction, null);
+ underTest.notifyPostBusinessEvent(event);
+ verify(postListener).onBusinessEvent(event);
+ verifyNoInteractions(externalEventService);
+ // simulate nested transaction
+ underTest.afterBegin(mockTransaction, null);
+ underTest.notifyPostBusinessEvent(nestedEvent);
+ verify(postListener).onBusinessEvent(nestedEvent);
+ verifyNoInteractions(externalEventService);
+ // simulate commit nested transaction
+ underTest.beforeCommit(mockTransaction);
+ underTest.afterCommit(mockTransaction, null);
+ verify(externalEventService).postEvent(nestedEvent);
+ // simulate commit outer transaction
+ underTest.beforeCommit(mockTransaction);
+ verify(externalEventService).postEvent(event);
+ underTest.afterCommit(mockTransaction, null);
+ verifyNoInteractions(mockTransaction);
+ }
+
+ @Test
+ public void
testNotifyPostBusinessEventShouldCollectEventsWithinTransactionInNestedRollbackTransaction()
{
+ // given
+ setBusinessDate();
+ configureExternalEventsProperties(true);
+ MockBusinessEvent event = new MockBusinessEvent();
+ MockBusinessEvent nestedEvent = new MockBusinessEvent();
+ BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+ underTest.addPostBusinessEventListener(MockBusinessEvent.class,
postListener);
+ TransactionExecution mockTransaction =
mock(TransactionExecution.class);
+ // when
+
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+ .thenReturn(new ExternalEventConfiguration("aType", true));
+ when(transactionHelper.hasTransaction()).thenReturn(true);
+
+ // simulate outer transaction
+ underTest.afterBegin(mockTransaction, null);
+ underTest.notifyPostBusinessEvent(event);
+ verify(postListener).onBusinessEvent(event);
+ verifyNoInteractions(externalEventService);
+ // simulate nested transaction
+ underTest.afterBegin(mockTransaction, null);
+ underTest.notifyPostBusinessEvent(nestedEvent);
+ verify(postListener).onBusinessEvent(nestedEvent);
+ verifyNoInteractions(externalEventService);
+ // simulate commit nested transaction
+ underTest.afterRollback(mockTransaction, null);
+ verifyNoInteractions(externalEventService);
+ // simulate commit outer transaction
+ underTest.beforeCommit(mockTransaction);
+ verify(externalEventService).postEvent(event);
+ underTest.afterCommit(mockTransaction, null);
+ verifyNoInteractions(mockTransaction);
+ }
+
+ @Test
+ public void
testNotifyPostBusinessEventShouldCollectEventsWithinTransactionAndNotSendExternalOnRollback()
{
+ // given
+ setBusinessDate();
+ configureExternalEventsProperties(true);
+ MockBusinessEvent event = new MockBusinessEvent();
+ BusinessEventListener<MockBusinessEvent> postListener = mockListener();
+ underTest.addPostBusinessEventListener(MockBusinessEvent.class,
postListener);
+ TransactionExecution mockTransaction =
mock(TransactionExecution.class);
+ underTest.afterBegin(mockTransaction, null);
+ // when
+
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
+ .thenReturn(new ExternalEventConfiguration("aType", true));
+ when(transactionHelper.hasTransaction()).thenReturn(true);
+ underTest.notifyPostBusinessEvent(event);
+ // then
+ verify(postListener).onBusinessEvent(event);
+ verifyNoInteractions(externalEventService);
+ // simulate rollback transaction
+ verifyNoInteractions(externalEventService);
+ underTest.afterRollback(mockTransaction, null);
+ verifyNoInteractions(externalEventService);
+ verifyNoInteractions(mockTransaction);
+ }
+
@Test
public void testNotifyPostBusinessEventShouldNotifyPostListeners() {
// given
@@ -85,6 +221,7 @@ class BusinessEventNotifierServiceImplTest {
BusinessEventListener<MockBusinessEvent> postListener = mockListener();
underTest.addPostBusinessEventListener(MockBusinessEvent.class,
postListener);
+ when(transactionHelper.hasTransaction()).thenReturn(false);
when(externalEventConfigurationRepository.findExternalEventConfigurationByTypeWithNotFoundDetection(Mockito.any()))
.thenReturn(new ExternalEventConfiguration("aType", true));
// when
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
index 98f9ab0b1..abb535e37 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
@@ -46,8 +46,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class
})
public class CustomSnapshotEventIntegrationTest extends
BaseLoanIntegrationTest {
- private SchedulerJobHelper schedulerJobHelper = new
SchedulerJobHelper(this.requestSpec);
- private Gson gson = new Gson();
+ private final SchedulerJobHelper schedulerJobHelper = new
SchedulerJobHelper(this.requestSpec);
@Test
public void testSnapshotEventGenerationWhenLoanInstallmentIsNotPayed() {
@@ -221,9 +220,8 @@ public class CustomSnapshotEventIntegrationTest extends
BaseLoanIntegrationTest
public void
testNoSnapshotEventGenerationWhenCOBDateIsNotMatchingWithInstallmentDueDate() {
runAt("30 January 2023", () -> {
// Enable Business Step
- enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS",
"LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
- "CHECK_LOAN_REPAYMENT_OVERDUE",
"UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
- "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
+ enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS",
"LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_OVERDUE",
+ "UPDATE_LOAN_ARREARS_AGING",
"ADD_PERIODIC_ACCRUAL_ENTRIES", "EXTERNAL_ASSET_OWNER_TRANSFER",
"CHECK_DUE_INSTALLMENTS");
enableLoanAccountCustomSnapshotBusinessEvent();
@@ -265,6 +263,8 @@ public class CustomSnapshotEventIntegrationTest extends
BaseLoanIntegrationTest
@Test
public void
testNoSnapshotEventGenerationWhenCustomSnapshotEventIsDisabled() {
runAt("31 January 2023", () -> {
+ // disable custom snapshot event
+ disableLoanAccountCustomSnapshotBusinessEvent();
// Enable Business Step
enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS",
"LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
"CHECK_LOAN_REPAYMENT_OVERDUE",
"UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
@@ -333,6 +333,14 @@ public class CustomSnapshotEventIntegrationTest extends
BaseLoanIntegrationTest
Assertions.assertTrue(updatedConfigurations.get("LoanAccountCustomSnapshotBusinessEvent"));
}
+ private void disableLoanAccountCustomSnapshotBusinessEvent() {
+ final Map<String, Boolean> updatedConfigurations =
ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
+ responseSpec,
"{\"externalEventConfigurations\":{\"LoanAccountCustomSnapshotBusinessEvent\":false}}\n");
+ Assertions.assertEquals(updatedConfigurations.size(), 1);
+
Assertions.assertTrue(updatedConfigurations.containsKey("LoanAccountCustomSnapshotBusinessEvent"));
+
Assertions.assertFalse(updatedConfigurations.get("LoanAccountCustomSnapshotBusinessEvent"));
+ }
+
private void updateBusinessDateAndExecuteCOBJob(String date) {
businessDateHelper.updateBusinessDate(
new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));