Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird (Loop) behavior when using Spring @TransactionalEventListener to publish event

I have a weird issue which involves @TransactionalEventListener not firing correctly or behavior as expected when triggered by another @TransactionalEventListener.

The general flow is:

  • AccountService publish an Event (to AccountEventListener)
  • AccountEventListener listens for the Event
  • Perform some processing and then publish another Event (to MailEventListener)
  • MailEventListener listens for the Event and peform some processing

So here's the classes (excerpt).

public class AccountService {

    @Transactional
    public User createAccount(Form registrationForm) {

        // Some processing

        // Persist the entity
        this.accountRepository.save(userAccount);

        // Publish the Event
        this.applicationEventPublisher.publishEvent(new RegistrationEvent());
    }
}

public class AccountEventListener {

    @TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public MailEvent onAccountCreated(RegistrationEvent registrationEvent) {

        // Some processing

        // Persist the entity
        this.accountRepository.save(userAccount);

        return new MailEvent();
    }
}

public class MailEventListener {

    private final MailService mailService;

    @Async
    @EventListener
    public void onAccountCreated(MailEvent mailEvent) {

        this.mailService.prepareAndSend(mailEvent);
    }
}

This code works but my intention is to use @TransactionalEventListener in my MailEventListener class. Hence, the moment I change from @EventListener to @TransactionalEventListener in MailEventListener class. The MailEvent does not get triggered.

public class MailEventListener {

    private final MailService mailService;

    @Async
    @TransactionalEventListener
    public void onAccountCreated(MailEvent mailEvent) {

        this.mailService.prepareAndSend(mailEvent);
    }
}

MailEventListener was never triggered. So I went to view Spring Documentation, and it states that @Async @EventListener is not support for event that is published by the return of another event. And so I changed to using ApplicationEventPublisher in my AccountEventListener class.

public class AccountEventListener {

    @TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void onAccountCreated(RegistrationEvent registrationEvent) {

        // Some processing

        this.accountRepository.save(userAccount);

        this.applicationEventPublisher.publishEvent(new MailEvent());
    }
}

Once I changed to the above, my MailEventListener now will pick up the event that is sent from AccountEventListener but the webpage hangs when form is submitted and it throws some exception after awhile, and then it also sent me about 9 of the same email to my email account.

I added some logging, and found out that my AccountEventListener (this.accountRepository.save()) actually ran 9 times before hitting the exception, which then causes my MailEventListener to execute 9 times I believe, and that is why I received 9 mails in my inbox.

Here's the logs in Pastebin.

I'm not sure why and what is causing it to run 9 times. There is no loop or anything in my methods, be it in AccountService, AccountEventListener, or MailEventListener.

Thanks!

like image 608
user1778855 Avatar asked Aug 09 '18 14:08

user1778855


People also ask

Are spring events asynchronous?

Spring allows us to create and publish custom events that by default are synchronous. This has a few advantages, such as the listener being able to participate in the publisher's transaction context.

What is spring event listener?

A listener can be defined in two ways. Either the @EventListener annotation or the ApplicationListener interface can be used. Spring enables to create and publish custom events that are synchronous by default. This means that the publisher thread is blocked until all listeners have completed processing the event.


1 Answers

So I went to view Spring Documentation, and it states that @Async @EventListener is not support for event that is published by the return of another event. And so I changed to using ApplicationEventPublisher in my AccountEventListener class.

Your understand is incorrect.

The document said that:

This feature is not supported for asynchronous listeners.

It does not mean

it states that @Async @EventListener is not support for event that is published by the return of another event.

It means:

This feature does not support events return from @Async @EventListener.

Your setup:

@Async
@TransactionalEventListener
public void onAccountCreated(MailEvent mailEvent) {

    this.mailService.prepareAndSend(mailEvent);
}

Does not work because as stated in document:

If the event is not published within the boundaries of a managed transaction, the event is discarded unless the fallbackExecution() flag is explicitly set. If a transaction is running, the event is processed according to its TransactionPhase.

If you use the debug, you can see that if your event is returned from an event listener, it happens after the transaction commit, hence the event is discarded.

So if you set the fallbackExecution = true as stated in the document, your event will correctly listened:

@Async
@TransactionalEventListener(fallbackExecution = true)
public void onAccountCreated(MailEvent mailEvent) {

    this.mailService.prepareAndSend(mailEvent);
}

The repeated behavior is look like some retry behavior, the connection queued up, exhaust the pool and throw the exception. Unless you provide a minimal source code to reproduce the problem, I can't identify it.

Update

Reading your code, the root cause is clear now.

Look at your setup for POST /registerPublisherCommon

  1. MailPublisherCommonEvent and AccountPublisherCommonEvent are subevent of BaseEvent
  2. createUserAccountPublisherCommon publish an event of type AccountPublisherCommonEvent
  3. MailPublisherCommonEventListener is registered to handle MailPublisherCommonEvent
  4. AccountPublisherCommonEventListener is registered to handle BaseEvent and ALL SUB-EVENT of it.
  5. AccountPublisherCommonEventListener also publishes MailPublisherCommonEvent (which is also a BaseEvent).

Read 4 + 5 you will see the root cause: AccountPublisherCommonEventListener publishes MailPublisherCommonEvent which is also handled by itself, hence the infinite event processing occur.

To resolve it, simply narrow down the type of event it can handle like you did.

Note

Your setup for MailPublisherCommonEvent working regardless the fallbackExecution flag because you're publishing it INSIDE A TRANSACTION, not OUTSIDE A TRANSACTION (by return from an event listener) like you specified in your question.

like image 196
Mạnh Quyết Nguyễn Avatar answered Oct 11 '22 21:10

Mạnh Quyết Nguyễn