Note: read the end of the answer for the way I implemented @Nonika's suggestions
What's the "right way" to send a websocket event on data insert?
I'm using a Spring Boot server with SQL/JPA and non-stomp websockets. I need to use "plain" websockets as I'm using Java clients where (AFAIK) there's no stomp support.
When I make a change to the database I need to send the event to the client so I ended up with an implementation like this:
@Transactional
public void addEntity(...) {
performActualEntityAdding();
sendEntityAddedEvent(eventData);
}
@Transactional
public void sendEntityAddedEvent(String eventData) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
sendEntityAddedEventAsync(eventData);
}
});
}
@Async
public void sendEntityAddedEventAsync(String eventData) {
// does the websocket session sending...
}
This works. If I would just call the sendEntityAddedEventAsync
it would also work for real world scenarios but it fails on unit tests because the event would arrive before transaction commit. As such when the unit test invokes a list of the entities after the event it fails.
This feels like a hack that shouldn't be here. Is there a better way to ensure a commit? I tried multiple alternative approaches and the problem is that they often worked for 10 runs of the unit tests yet failed every once in a while. That isn't acceptable.
I tried multiple approaches to solve this such as different transaction annotations and splitting the method to accommodate them. E.g read uncommitted, not supported (to force a commit) etc. Nothing worked for all cases and I couldn't find an authoritative answer for this (probably common) use case that wasn't about STOMP (which is pretty different).
Edit
One of my original attempts looked something like this:
// this shouldn't be in a transaction
public void addEntity(...) {
performActualEntityAdding();
sendEntityAddedEvent(eventData);
}
@Transactional
public void performActualEntityAdding(...) {
//....
}
@Async
public void sendEntityAddedEventAsync(String eventData) {
// does the websocket session sending...
}
The assumption here is that when sendEntityAddedEventAsync
is invoked the data would already be in the database. It wasn't for a couple of additional milliseconds.
A few additional details:
- Test environment is based on h2 (initially I mistakenly wrote hsql)
- Project is generated by JHipster
- Level 2 cache is used but disabled as NONE for these entities
Solution (based on @Nonika's answer):
The solution for me included something similar to this:
public class WebEvent extends ApplicationEvent {
private ServerEventDAO event;
public WebEvent(Object source, ServerEventDAO event) {
super(source);
this.event = event;
}
public ServerEventDAO getEvent() {
return event;
}
}
@Transactional
public void addEntity(...) {
performActualEntityAdding();
applicationEventPublisher.publishEvent(new WebEvent(this, evtDao));
}
@Async
@TransactionalEventListener
public void sendEntityAddedEventAsync(WebEvent eventData) {
// does the websocket session sending...
}
This effectively guarantees that the data is committed properly before sending the event and it runs asynchronously to boot. Very nice and simple.