Skip to content

Commit

Permalink
Introduce and throw EventExecutionException
Browse files Browse the repository at this point in the history
Construct an exception dedicated for event handling failures. This
exception should then carry the required information to store events as
dead letters, with things like the sequenceId and processingGroup. The
exception should be thrown from the SimpleEventHandlerInvoker, that
wraps the original exception of the event handler in it.

#2021
  • Loading branch information
smcvb committed Nov 26, 2021
1 parent cda3bd4 commit b0064b6
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2010-2021. Axon Framework
*
* Licensed 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.axonframework.eventhandling;

import org.axonframework.messaging.HandlerExecutionException;

/**
* An {@link HandlerExecutionException} implementation indicating an exception occurred during event handling. This
* exception is typically used to wrap checked exceptions thrown from an Event Handler.
*
* @author Steven van Beelen
* @since 4.6.0
*/
public class EventExecutionException extends HandlerExecutionException {

private static final long serialVersionUID = -7002989047107235088L;

// TODO: 26-11-21 group into ProcessingIdentifier?
private final String sequenceIdentifier;
private final String processingGroup;

/**
* Constructs an {@link EventExecutionException} using the provided {@code message}, {@code cause}, {@code
* sequenceIdentifier} and {@code processingGroup}. Combines the {@code sequenceIdentifier} and {@code
* processingGroup} with a dash ({@code `-`}) as the {@code details} of this {@link HandlerExecutionException}.
*
* @param message the message describing the exception
* @param cause the cause of the exception
* @param sequenceIdentifier the identifier defining the sequence of the event for which execution failed
* @param processingGroup the name of the processing group the exception occurred in
*/
public EventExecutionException(String message,
Throwable cause,
String sequenceIdentifier,
String processingGroup) {
super(message, cause, sequenceIdentifier + "-" + processingGroup);
this.sequenceIdentifier = sequenceIdentifier;
this.processingGroup = processingGroup;
}

/**
* Returns the sequence identifier of the event for which execution failed.
*
* @return the sequence identifier of the event for which execution failed
*/
public String getSequenceIdentifier() {
return sequenceIdentifier;
}

/**
* Returns the name of processing group this exception occurred in.
*
* @return the name of processing group this exception occurred in
*/
public String getProcessingGroup() {
return processingGroup;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020. Axon Framework
* Copyright (c) 2010-2021. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -112,12 +112,17 @@ public void handle(EventMessage<?> message, Segment segment) throws Exception {
try {
handler.handle(message);
} catch (Exception e) {
listenerInvocationErrorHandler.onError(e, message, handler);
listenerInvocationErrorHandler.onError(executionException(message, e), message, handler);
}
}
}
}

private EventExecutionException executionException(EventMessage<?> event, Exception exception) {
String message = String.format("Handling failed for event [%s] in group [%s]", event, processingGroup);
return new EventExecutionException(message, exception, sequenceIdentifier(event).toString(), processingGroup);
}

@Override
public boolean canHandle(EventMessage<?> eventMessage, Segment segment) {
return hasHandler(eventMessage) && sequencingPolicyMatchesSegment(eventMessage, segment);
Expand Down Expand Up @@ -148,10 +153,11 @@ public boolean supportsReset() {
}

private boolean sequencingPolicyMatchesSegment(EventMessage<?> message, Segment segment) {
return segment.matches(Objects.hashCode(getOrDefault(
sequencingPolicy.getSequenceIdentifierFor(message),
message::getIdentifier)
));
return segment.matches(Objects.hashCode(sequenceIdentifier(message)));
}

private Object sequenceIdentifier(EventMessage<?> event) {
return getOrDefault(sequencingPolicy.getSequenceIdentifierFor(event), event::getIdentifier);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020. Axon Framework
* Copyright (c) 2010-2021. Axon Framework
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,13 +16,16 @@

package org.axonframework.eventhandling;

import org.axonframework.common.AxonConfigurationException;
import org.axonframework.eventhandling.async.SequencingPolicy;
import org.junit.jupiter.api.*;
import org.mockito.*;

import java.util.List;

import static org.axonframework.utils.EventTestUtils.createEvent;
import static org.axonframework.utils.EventTestUtils.createEvents;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
Expand All @@ -33,6 +36,7 @@
class SimpleEventHandlerInvokerTest {

private static final Object NO_RESET_PAYLOAD = null;
private static final String PROCESSING_GROUP = "processingGroup";

private EventMessageHandler mockHandler1;
private EventMessageHandler mockHandler2;
Expand All @@ -45,6 +49,7 @@ void setUp() {
mockHandler2 = mock(EventMessageHandler.class);
testSubject = SimpleEventHandlerInvoker.builder()
.eventHandlers("test", mockHandler1, mockHandler2)
.processingGroup(PROCESSING_GROUP)
.build();
}

Expand Down Expand Up @@ -76,6 +81,46 @@ void testRepeatedEventPublication() throws Exception {
inOrder.verifyNoMoreInteractions();
}

@Test
void testHandleWrapsExceptionInEventExecutionException() throws Exception {
// given...
ListenerInvocationErrorHandler errorHandler = mock(ListenerInvocationErrorHandler.class);
//noinspection unchecked
SequencingPolicy<EventMessage<?>> sequencingPolicy = mock(SequencingPolicy.class);

SimpleEventHandlerInvoker customTestSubject =
SimpleEventHandlerInvoker.builder()
.eventHandlers(mockHandler1)
.listenerInvocationErrorHandler(errorHandler)
.sequencingPolicy(sequencingPolicy)
.processingGroup(PROCESSING_GROUP)
.build();

EventMessage<?> testEvent = createEvent();

RuntimeException expectedException = new RuntimeException("some-exception");
String expectedSequenceIdentifier = "sequenceIdentifier";

when(mockHandler1.handle(testEvent)).thenThrow(expectedException);
when(sequencingPolicy.getSequenceIdentifierFor(testEvent)).thenReturn(expectedSequenceIdentifier);

// when...
customTestSubject.handle(testEvent, Segment.ROOT_SEGMENT);

// then...
ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class);

verify(errorHandler).onError(exceptionCaptor.capture(), eq(testEvent), eq(mockHandler1));

Exception result = exceptionCaptor.getValue();

assertTrue(result instanceof EventExecutionException);
EventExecutionException executionException = ((EventExecutionException) result);
assertEquals(expectedSequenceIdentifier, executionException.getSequenceIdentifier());
assertEquals(PROCESSING_GROUP, executionException.getProcessingGroup());
assertEquals(expectedException, executionException.getCause());
}

@Test
void testPerformReset() {
testSubject.performReset();
Expand All @@ -93,4 +138,25 @@ void testPerformResetWithResetContext() {
verify(mockHandler1).prepareReset(eq(resetContext));
verify(mockHandler2).prepareReset(eq(resetContext));
}

@Test
void testBuildWithNullProcessingGroupThrowsAxonConfigurationException() {
SimpleEventHandlerInvoker.Builder testSubject = SimpleEventHandlerInvoker.builder();

assertThrows(AxonConfigurationException.class, () -> testSubject.processingGroup(null));
}

@Test
void testBuildWithEmptyProcessingGroupThrowsAxonConfigurationException() {
SimpleEventHandlerInvoker.Builder testSubject = SimpleEventHandlerInvoker.builder();

assertThrows(AxonConfigurationException.class, () -> testSubject.processingGroup(""));
}

@Test
void testBuildWithoutProcessingGroupThrowsAxonConfigurationException() {
SimpleEventHandlerInvoker.Builder testSubject = SimpleEventHandlerInvoker.builder();

assertThrows(AxonConfigurationException.class, testSubject::build);
}
}

0 comments on commit b0064b6

Please sign in to comment.