I have a weird issue which involves @TransactionalEventListener
not firing correctly or behavior as expected when triggered by another @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.
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!
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.
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.
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
MailPublisherCommonEvent
and AccountPublisherCommonEvent
are subevent of BaseEvent
createUserAccountPublisherCommon
publish an event of type AccountPublisherCommonEvent
MailPublisherCommonEventListener
is registered to handle MailPublisherCommonEvent
AccountPublisherCommonEventListener
is registered to handle BaseEvent
and ALL SUB-EVENT of it.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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With