0

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.

Shai Almog
  • 51,749
  • 5
  • 35
  • 65
  • Also you can use `try` and `catch` for checking if the data was committed or not, if not then an implementation of `PersistenceException` should be thrown, also if you are using `hibernate` then might implement a `listener after saved`. Then based your question is very own of a software developer, No "Right way". – Jonathan JOhx Nov 24 '19 at 04:53
  • The request happens asynchronously from the client. It would be surprising to me if there's no officially sanctioned way to implement such a use case. – Shai Almog Nov 24 '19 at 06:46
  • So far I know, there is no officially sanctioned way to implement such a use case, using `STOMP` would be the closest one. – Jonathan JOhx Nov 24 '19 at 07:32
  • The unit tests sound like the problem. What do they look like? Also, why is `sendEntityAddedEvent` marked as `@Transactional`? – Andy Wilkinson Nov 24 '19 at 07:57
  • We need it to be transnational so the `registerSynchronization` call will work. I tried various combinations of method calls where the parent method isn't transactional and it invokes the save method which is transactional then invokes a separate non-transactional event method. Surprisingly this didn't work. The tests are completely external and work with the webservice/websocket. The problem with them is that they are fast. With the current setup they work so I'm pretty sure the problem was in the server. – Shai Almog Nov 24 '19 at 08:02
  • The transaction from `addEntity` should be active when it calls `sendEntityAddedEvent`. It sounds like your transaction boundaries are wrong. Can you provide a complete example? – Andy Wilkinson Nov 24 '19 at 10:25
  • Sure, I edited the question with some more details of the original approach I tried which AFAIK **should** work but didn't. – Shai Almog Nov 24 '19 at 10:42
  • Are addEntity() and performActualEntityAdding() in the same class file? if so this way @Transactional on performActualEntityAdding() will not work. – Nonika Nov 24 '19 at 15:44
  • I used separate session beans – Shai Almog Nov 24 '19 at 19:18

1 Answers1

1

Spring is using AdviceMode.PROXY for both @Async and @Transactional this is quote from the javadoc:

The default is AdviceMode.PROXY. Please note that proxy mode allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way; an Async annotation on such a method within a local call will be ignored since Spring's interceptor does not even kick in for such a runtime scenario. For a more advanced mode of interception, consider switching this to AdviceMode.ASPECTJ.

This rule is common for almost all spring annotations which requires proxy to operate.

Into your first example, you have a @Transactional annotation on both addEntity(..) and performActualEntityAdding(..). I suppose you call addEntity from another class so @Transactional works as expected. process in this scenario can be described in this flow

// -> N1 transaction starts

addEntity(){             
    performActualEntityAdding()//-> we are still in transaction N1
    sendEntityAddedEvent()  //  ->  call to this @Async is a class local call, so this advice is ignored. But if this was an async call this would not work either.
}
//N1 transaction commits;

That's why the test fails. it gets an event that there is a change into the db, but there is nothing because the transaction has not been committed yet.

Scenario 2.

When you don't have a @Transactional addEntity(..) then second transaction for performActualEntityAdding not starts as there is a local call too.

Options:

  • You can use some middleware class to call these methods to trigger spring interceptors.
  • you can use Self injection with Spring

    if you have Spring 5.0 there is handy @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

Nonika
  • 2,490
  • 13
  • 15
  • Thanks. These were in separate beans though, sorry for not clarifying that. The events are fired from an EventSession bean. I'm not sure how I can apply the `TransactionalEventListener`, the docs are a bit unclear here. Can I just annotate the `sendEntityAddedEvent` with `TransactionalEventListener` and then remove the inner class to invoke the async method directly? – Shai Almog Nov 24 '19 at 19:25
  • Check this article https://panlw.github.io/15459049161482.html – Nonika Nov 24 '19 at 19:29
  • I have one question does your test environment use embedded hsql? or dedicated one? – Nonika Nov 24 '19 at 19:33
  • 1
    Thanks, this took a bit of experimentation but it seems to work nicely with `TransactionalEventListener` and it's much more elegant. I'll edit my question with my current code for reference – Shai Almog Nov 25 '19 at 12:50