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

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP



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.


@TransactionalEventListener


@TransactionalEventListener



The general flow is:



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.


@TransactionalEventListener


MailEventListener


@EventListener


@TransactionalEventListener


MailEventListener


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.


MailEventListener


@Async @EventListener


ApplicationEventPublisher


AccountEventListener


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.


MailEventListener


AccountEventListener



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.


AccountEventListener


this.accountRepository.save()


MailEventListener



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.


AccountService


AccountEventListener


MailEventListener



Thanks!




2 Answers
2



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:


fallbackExecution = true


@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


POST /registerPublisherCommon


MailPublisherCommonEvent


AccountPublisherCommonEvent


BaseEvent


createUserAccountPublisherCommon


AccountPublisherCommonEvent


MailPublisherCommonEventListener


MailPublisherCommonEvent


AccountPublisherCommonEventListener


BaseEvent


AccountPublisherCommonEventListener


MailPublisherCommonEvent


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.


AccountPublisherCommonEventListener


MailPublisherCommonEvent



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.


MailPublisherCommonEvent


fallbackExecution


INSIDE A TRANSACTION


OUTSIDE A TRANSACTION





Hi, thanks for looking over it. I will have a working copy in github later in the day once I get it up.
– user1778855
Aug 12 at 5:22





Hi, I have setup a git, and actually found out the cause and solution, just not exactly sure of the exact reasoning behind it. if you could take a look at my reply above. Thank you.
– user1778855
Aug 12 at 9:54





Read my updated answer
– Mạnh Quyết Nguyễn
Aug 12 at 10:39





I saw the mistake now. I actually didn't realize that I had set all to the same BaseEvent even after doing the POC which is the cause of the loop. And, yes, I need to be inside the transaction and not outside. Thanks for pointing out to me.
– user1778855
Aug 12 at 11:06






You're welcome~
– Mạnh Quyết Nguyễn
Aug 12 at 11:08



For what it's worth, I found out what is causing the looping and how to resolve it but I still cannot understand why does it happens as such.



And correct me if I'm wrong, setting fallbackExecution = true isn't really the answer to the issue.


fallbackExecution = true



Based on Spring documentation, the event is processed according to its TransactionPhase. So I had @Transactional(propagation = Propagation.REQUIRES_NEW) in my AccountEventListener class which should be a transaction by itself, and MailEventListener should only be executing in the event that the phase which by default is AFTER_COMMIT for @TransactionalEventListener.


the event is processed according to its TransactionPhase.


@Transactional(propagation = Propagation.REQUIRES_NEW)


AccountEventListener


MailEventListener


AFTER_COMMIT


@TransactionalEventListener



I setup a git, to reproduce the issue and while doing so, allows me to discover what really went wrong. Having said that, I still do not understand the root cause of it.



Before I do, there are some things that I am not 100% sure but it's just my guess/understand at this moment.



As mentioned in the Spring Documentation,



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.



And my guess of the reason why MailEventListener class did not pick up the event when using the event as the return type to let Spring automatically publish is because it publishes outside of the boundaries of a managed transaction. Which is why, if you set (fallbackExecution = true) in MailEventListener, it will work/run because it doesn't matter if it in within the transaction or not.


MailEventListener


(fallbackExecution = true)


MailEventListener



Note: Classes mentioned above are taken from my initial post. The
classes below are named slightly differently but essentially, all are
still the same, just different name.



Now, back to the point where I said I found the answer as to why it is causing the loop.
Basically, it is when the parameter put in the listener is a BaseEvent of sort.


BaseEvent



So assuming that I have the following classes:


public class BaseEvent

private final User userAccount;



public class AccountPublisherCommonEvent extends BaseEvent

public AccountPublisherCommonEvent(User userAccount)
super(userAccount);




public class MailPublisherCommonEvent extends BaseEvent

public MailPublisherCommonEvent(User userAccount)
super(userAccount);





And the listeners classes (Notice that the parameter is the BaseEvent):


(Notice that the parameter is the BaseEvent)


public class AccountPublisherCommonEventListener

private final AccountRepository accountRepository;
private final ApplicationEventPublisher eventPublisher;

// Notice that the parameter is the BaseEvent
@TransactionalEventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onAccountPublisherCommonEvent(BaseEvent accountEvent)

User userAccount = accountEvent.getUserAccount();
userAccount.setUserFirstName("common");

this.accountRepository.save(userAccount);

this.eventPublisher.publishEvent(new MailPublisherCommonEvent(userAccount));




public class MailPublisherCommonEventListener

@Async
@TransactionalEventListener
public void onMailPublisherCommonEvent(MailPublisherCommonEvent mailEvent)

log.info("Sending common email ...");




Basically, if the setup of the listener is as such (above), then you enter a loop and hit an exception as mentioned by the previous poster.



The repeated behavior is look like some retry behavior, the connection queued up, exhaust the pool and throw the exception.



And to resolve the issue, simply, change the input, and define the classes to listen by (Notice the addition of (AccountPublisherCommonEvent.class)):


(Notice the addition of (AccountPublisherCommonEvent.class))


public class AccountPublisherCommonEventListener

private final AccountRepository accountRepository;
private final ApplicationEventPublisher eventPublisher;

// Notice the addition of (AccountPublisherCommonEvent.class)
@TransactionalEventListener(AccountPublisherCommonEvent.class)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onAccountPublisherCommonEvent(BaseEvent accountEvent)

User userAccount = accountEvent.getUserAccount();
userAccount.setUserFirstName("common");

this.accountRepository.save(userAccount);

this.eventPublisher.publishEvent(new MailPublisherCommonEvent(userAccount));





An alternative would be changing the parameter to the actual class name instead of the BaseEvent class I suppose. And there is no changes required to the MailPublisherCommonEventListener


BaseEvent


MailPublisherCommonEventListener



By doing so, it no longer loop, nor hit the exception. The behavior would run as I want and expected it to.



I would appreciate if anyone could answer to as of why does this happen exactly if I place the BaseEvent as the input would caused a looping to occur. Here's the link to git for the poc. Hope I'm making some sense here.


BaseEvent



Thank you.






By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Popular posts from this blog

Firebase Auth - with Email and Password - Check user already registered

Dynamically update html content plain JS

How to determine optimal route across keyboard