Dead-Letter Error Handling

Hi,

I am looking for a way to add an (ListenerInvocation)ErrorHandler to an event processor with dead-letter enabled.

The documented way (Spring Boot) to add an error handler does not work:

@Bean
  public ConfigurerModule errorHandlerConfigurer() {
    return configurer ->
        configurer
            .eventProcessing()
            .registerDefaultErrorHandler(
                conf ->
                    (errorContext) -> {
                      log.atError()
                          .log("error handling message: {}", errorContext.error().getMessage());
                    })

For a normal event handler (without DLQ) this works just fine. Through debugging I found it does nothing when dlq.enabled: true for this processing group (so a DeadLetteringEventHandlerInvoker is instantiated with a propagating error handler, not the one configured above).

Question: Is this intended behavior?
If so, how can I add error handling behavior to a dead-letter event processor?

The ListenerInvocationErrorHandler is not supported for Event Handling Components that have dead-lettering enabled, @mbechto.

To be quite frank with you, I thought we added that to the dead-letter queue section of our documentation, but I see that’s not the case. Nonetheless, let me explain to you why we took this decision.

The ListenerInvocationErrorHandler was our initial solution to give users the “power” to decide what to do with event handling failures. You could bubble up the exception, simply log the predicament, or retry right there, on the spot.

As time moved on, we figured a DLQ for Event Handling Components would be a great addition. Initially, we assumed we could simply make a new type of ListenerInvocationErrorHandler and be done with it. This was, however, not possible.

The reason it’s not possible is because we want to ensure that if one event in a sequence fails, all subsequent events that belong to that sequence are also not handled. Hence, right before we invoke your Event Handlers, the referred to DeadLetteringEventHandlerInvoker will first check if the given event is part of a dead-lettered sequence.

As the ListenerInvocationErrorHandler only listens when something goes, it wouldn’t suffice for the sequenced dead-lettering behavior we were looking for.

Hence, we had to go one layer deeper, being the EventHandlerInvoker, which constituted in the DeadLetteringEventHandlerInvoker you’ve found too.

To circle back: as we require quite some control in the EventHandlerInvoker to ensure the dead-lettering behavior is like it is, we decided that omitting the options given by the (suboptimal) ListenerInvocationErrorHandler was the right choice.

Depending on what you want to achieve when an event fails, you’re best bet is to customize the EnqueuePolicy. The EnqueuePolicy will make the decision what to do with failed events, so would be your best bet to add additional behavior.

I hope all that clarifies thing sufficiently for you, @mbechto!

1 Like

Thanks for coming back to me @Steven_van_Beelen :slight_smile:

Your explanation makes sense to me and the decision is perfectly understandable. As for the use case: We were trying to register a default ListenerInvocationErrorHandler in our Spring Boot integration tests as a means to detect if something went wrong in a test scenario - that being not so trivial considering asynchronous event handling and, sometimes we want to verify that a command has no effect (a kind of test to be avoided where possible, of course).

Simply put: By default, we wanted an integration test scenario to fail when an event handler throws an exception. Meanwhile, we settled for the EventProcessingConfiguration API to get all processors and their DLQ, if any and validate that the queues are empty. Not sure if that’s related to best practice but it seems to work well enough.

Hope that shed some light on the topic. Cheers!

1 Like

Checking for empty queues is a fine solution I think. Because if something’s enqueued in the DLQ, that means event handling failed!

You could, for the sake of the integration tests, introduce a delegating EnqueuePolicy that does the validation for you, of course. Just as with the ListenerInvocationErrorHandler, there is an option to configure a default EnqueuePolicy that would be used by all Event Processors with a DLQ.

Nonetheless, your current approach seems fine to me as well. So feel free to pick whichever you prefer.