FINERACT-1694: Reworked bulk external events

This commit is contained in:
Arnold Galovics
2022-09-04 18:23:55 +02:00
committed by Arnold Gálovics
parent 0980aa8abf
commit 17b36a0919
5 changed files with 208 additions and 185 deletions
@@ -47,4 +47,7 @@ public interface BusinessEventNotifierService {
*/
<T extends BusinessEvent<?>> void addPostBusinessEventListener(Class<T> eventType, BusinessEventListener<T> listener);
void startExternalEventRecording();
void stopExternalEventRecording();
}
@@ -26,6 +26,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
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;
import org.apache.fineract.infrastructure.event.external.service.ExternalEventService;
import org.springframework.beans.factory.InitializingBean;
@@ -40,6 +41,9 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
private final Map<Class, List<BusinessEventListener>> preListeners = new HashMap<>();
private final Map<Class, List<BusinessEventListener>> postListeners = new HashMap<>();
private final ThreadLocal<Boolean> eventRecordingEnabled = ThreadLocal.withInitial(() -> false);
private final ThreadLocal<List<BusinessEvent<?>>> recordedEvents = ThreadLocal.withInitial(ArrayList::new);
private final ExternalEventService externalEventService;
private final FineractProperties fineractProperties;
@@ -54,6 +58,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
@Override
public void notifyPreBusinessEvent(BusinessEvent<?> businessEvent) {
throwExceptionIfBulkEvent(businessEvent);
List<BusinessEventListener> businessEventListeners = preListeners.get(businessEvent.getClass());
if (businessEventListeners != null) {
for (BusinessEventListener eventListener : businessEventListeners) {
@@ -74,6 +79,7 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
@Override
public void notifyPostBusinessEvent(BusinessEvent<?> businessEvent) {
throwExceptionIfBulkEvent(businessEvent);
List<BusinessEventListener> businessEventListeners = postListeners.get(businessEvent.getClass());
if (businessEventListeners != null) {
for (BusinessEventListener eventListener : businessEventListeners) {
@@ -82,14 +88,14 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
}
if (isExternalEventPostingEnabled()) {
// we only want to create external events for operations that were successful, hence the post listener
externalEventService.postEvent(businessEvent);
if (isExternalEventRecordingEnabled()) {
recordedEvents.get().add(businessEvent);
} else {
externalEventService.postEvent(businessEvent);
}
}
}
private boolean isExternalEventPostingEnabled() {
return fineractProperties.getEvents().getExternal().isEnabled();
}
@Override
public <T extends BusinessEvent<?>> void addPostBusinessEventListener(Class<T> eventType, BusinessEventListener<T> listener) {
List<BusinessEventListener> businessEventListeners = postListeners.get(eventType);
@@ -99,4 +105,37 @@ public class BusinessEventNotifierServiceImpl implements BusinessEventNotifierSe
}
businessEventListeners.add(listener);
}
private boolean isExternalEventRecordingEnabled() {
return eventRecordingEnabled.get();
}
private boolean isExternalEventPostingEnabled() {
return fineractProperties.getEvents().getExternal().isEnabled();
}
private void throwExceptionIfBulkEvent(BusinessEvent<?> businessEvent) {
if (businessEvent instanceof BulkBusinessEvent) {
throw new IllegalArgumentException("BulkBusinessEvent cannot be raised directly");
}
}
@Override
public void startExternalEventRecording() {
eventRecordingEnabled.set(true);
}
@Override
public void stopExternalEventRecording() {
eventRecordingEnabled.set(false);
try {
List<BusinessEvent<?>> recordedBusinessEvents = recordedEvents.get();
if (isExternalEventPostingEnabled()) {
log.debug("Posting the BulkBusinessEvent for the recorded {} events", recordedBusinessEvents.size());
externalEventService.postEvent(new BulkBusinessEvent(recordedBusinessEvents));
}
} finally {
recordedEvents.remove();
}
}
}
@@ -1,64 +0,0 @@
/**
* 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.external.service;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class DelayedExternalEventService {
private final ThreadLocal<List<BusinessEvent<?>>> localEventStorage = ThreadLocal.withInitial(ArrayList::new);
private final ExternalEventService delegate;
public <T> void enqueueEvent(BusinessEvent<T> event) {
if (event == null) {
throw new IllegalArgumentException("event cannot be null");
}
localEventStorage.get().add(event);
}
public boolean hasEnqueuedEvents() {
return !localEventStorage.get().isEmpty();
}
public void clearEnqueuedEvents() {
localEventStorage.get().clear();
}
public List<BusinessEvent<?>> getEnqueuedEvents() {
return List.copyOf(localEventStorage.get());
}
public void postEnqueuedEvents() {
List<BusinessEvent<?>> enqueuedEvents = localEventStorage.get();
if (enqueuedEvents.isEmpty()) {
throw new IllegalStateException("No events have been enqueued");
}
delegate.postEvent(new BulkBusinessEvent(enqueuedEvents));
}
}
@@ -0,0 +1,161 @@
/**
* 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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
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;
import org.apache.fineract.infrastructure.event.external.service.ExternalEventService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings({ "rawtypes", "unchecked" })
@MockitoSettings(strictness = Strictness.LENIENT)
class BusinessEventNotifierServiceImplTest {
@Mock
private ExternalEventService externalEventService;
@Mock
private FineractProperties fineractProperties;
@InjectMocks
private BusinessEventNotifierServiceImpl underTest;
@Test
public void testNotifyPostBusinessEventShouldNotifyPostListeners() {
// given
configureExternalEventsProperties(false);
MockBusinessEvent event = new MockBusinessEvent();
BusinessEventListener<MockBusinessEvent> postListener = mockListener();
underTest.addPostBusinessEventListener(MockBusinessEvent.class, postListener);
// when
underTest.notifyPostBusinessEvent(event);
// then
verify(postListener).onBusinessEvent(event);
verifyNoInteractions(externalEventService);
}
@Test
public void testNotifyPostBusinessEventShouldNotifyPostListenersAndPostAnExternalEvent() {
// given
configureExternalEventsProperties(true);
MockBusinessEvent event = new MockBusinessEvent();
BusinessEventListener<MockBusinessEvent> postListener = mockListener();
underTest.addPostBusinessEventListener(MockBusinessEvent.class, postListener);
// when
underTest.notifyPostBusinessEvent(event);
// then
verify(postListener).onBusinessEvent(event);
verify(externalEventService).postEvent(event);
}
@Test
public void testNotifyPostBusinessEventShouldNotifyPostListenersAndPostAnBulkExternalEventWhenRecordingEnabled() {
// given
configureExternalEventsProperties(true);
MockBusinessEvent event = new MockBusinessEvent();
BusinessEventListener<MockBusinessEvent> postListener = mockListener();
underTest.addPostBusinessEventListener(MockBusinessEvent.class, postListener);
underTest.startExternalEventRecording();
underTest.notifyPostBusinessEvent(event);
// when
underTest.stopExternalEventRecording();
// then
verify(postListener).onBusinessEvent(event);
ArgumentCaptor<BulkBusinessEvent> argumentCaptor = ArgumentCaptor.forClass(BulkBusinessEvent.class);
verify(externalEventService).postEvent(argumentCaptor.capture());
BulkBusinessEvent capturedEvent = argumentCaptor.getValue();
assertThat(capturedEvent.get()).hasSize(1);
assertThat(capturedEvent.get().get(0)).isEqualTo(event);
}
@Test
public void testNotifyPreBusinessEventShouldNotifyPreListeners() {
// given
configureExternalEventsProperties(false);
MockBusinessEvent event = new MockBusinessEvent();
BusinessEventListener<MockBusinessEvent> preListener = mockListener();
underTest.addPreBusinessEventListener(MockBusinessEvent.class, preListener);
// when
underTest.notifyPreBusinessEvent(event);
// then
verify(preListener).onBusinessEvent(event);
verifyNoInteractions(externalEventService);
}
@Test
public void testNotifyPreBusinessEventShouldNotifyPreListenersWithoutPostingAnExternalEvent() {
// given
configureExternalEventsProperties(true);
MockBusinessEvent event = new MockBusinessEvent();
BusinessEventListener<MockBusinessEvent> preListener = mockListener();
underTest.addPreBusinessEventListener(MockBusinessEvent.class, preListener);
// when
underTest.notifyPreBusinessEvent(event);
// then
verify(preListener).onBusinessEvent(event);
verifyNoInteractions(externalEventService);
}
private void configureExternalEventsProperties(boolean isExternalEventsEnabled) {
FineractProperties.FineractEventsProperties eventsProperties = new FineractProperties.FineractEventsProperties();
FineractProperties.FineractExternalEventsProperties externalProperties = new FineractProperties.FineractExternalEventsProperties();
eventsProperties.setExternal(externalProperties);
externalProperties.setEnabled(isExternalEventsEnabled);
given(fineractProperties.getEvents()).willReturn(eventsProperties);
}
private BusinessEventListener<MockBusinessEvent> mockListener() {
return (BusinessEventListener<MockBusinessEvent>) mock(BusinessEventListener.class);
}
private static class MockBusinessEvent implements BusinessEvent<Object> {
@Override
public Object get() {
return null;
}
@Override
public String getType() {
return null;
}
}
}
@@ -1,116 +0,0 @@
/**
* 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.external.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.util.List;
import org.apache.fineract.infrastructure.event.business.domain.BulkBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@SuppressWarnings({ "rawtypes", "unchecked" })
class DelayedExternalEventServiceTest {
@Mock
private ExternalEventService delegate;
@InjectMocks
private DelayedExternalEventService underTest;
@BeforeEach
public void setUp() {
underTest.clearEnqueuedEvents();
}
@Test
public void testEnqueueEventFailsWhenNullEventIsGiven() {
// given
// when & then
assertThatThrownBy(() -> underTest.enqueueEvent(null)).isExactlyInstanceOf(IllegalArgumentException.class);
}
@Test
public void testEnqueueEventWorks() {
// given
BusinessEvent event = mock(BusinessEvent.class);
// when
underTest.enqueueEvent(event);
// then
List<BusinessEvent<?>> enqueuedEvents = underTest.getEnqueuedEvents();
assertThat(enqueuedEvents).hasSize(1);
assertThat(enqueuedEvents.get(0)).isEqualTo(event);
}
@Test
public void testHasEnqueuedEventsReturnsTrueWhenEventIsEnqueued() {
// given
BusinessEvent event = mock(BusinessEvent.class);
underTest.enqueueEvent(event);
// when
boolean result = underTest.hasEnqueuedEvents();
// then
assertThat(result).isTrue();
}
@Test
public void testHasEnqueuedEventsReturnsFalseWhenNoEventIsEnqueued() {
// given
// when
boolean result = underTest.hasEnqueuedEvents();
// then
assertThat(result).isFalse();
}
@Test
public void testPostEnqueuedEventsFailsWhenNoEventIsEnqueued() {
// given
// when & then
assertThatThrownBy(() -> underTest.postEnqueuedEvents()).isExactlyInstanceOf(IllegalStateException.class);
}
@Test
public void testPostEnqueuedEventsWorks() {
// given
ArgumentCaptor<BusinessEvent> delegateEventCaptor = ArgumentCaptor.forClass(BusinessEvent.class);
BusinessEvent event = mock(BusinessEvent.class);
underTest.enqueueEvent(event);
// when
underTest.postEnqueuedEvents();
// then
verify(delegate).postEvent(delegateEventCaptor.capture());
BusinessEvent delegateEvent = delegateEventCaptor.getValue();
assertThat(delegateEvent).isExactlyInstanceOf(BulkBusinessEvent.class);
BulkBusinessEvent bulkDelegateEvent = (BulkBusinessEvent) delegateEvent;
List<BusinessEvent<?>> enqueuedEvents = bulkDelegateEvent.get();
assertThat(enqueuedEvents).hasSize(1);
assertThat(enqueuedEvents.get(0)).isEqualTo(event);
}
}