0
votes

I have a eventhandler method that is annotated @Transactional, this method calls an implementation of an event within the same class.

This event does some checks and depending on the outcome it will either do something, or change a status and throw a RuntimeException.

In case the status is changed because of the check, I need the status to stay persisted but the event to fail for retry.

The status change method is in another class and the method is annotated with @Transactional(propagation = Propagation.REQUIRES_NEW).

I would expect that because the inner transaction completes, the status change becomes persisted and the event transaction gets rolled back.

What I am seeing is that the status change is also rolled back but I don't get why it is rolling back everything when I explicity tell it to create a new transaction for the status change.

Keep in mind this is a legacy project, so major changes in the architecture are not possible.

I tried debugging the transaction changes and the debugger does jump into the commit of the new transaction, but for some reason it is not persisted into the database.

public class t implements it {
    // Do initialisation and class injection. Y is constructor injected
    private final Y y;

    public t(Y y) {
       this.y = y;
    }

    @Override
    @Transactional
    public void handleEvent(EventContext context) {
        switch (context.getEventType()) {
            case event:
                validate(context);
                break;
        }
    }

    private void validate(EventContext context) {
        Object o = crudService.findByProperty(context.getObjectUuid());
        if (!o.check) {
            y.changeStatus(ERROR);
            // break for retry
            throw new RuntimeException("Some serious message log");
        } else {
            // do some stuff
        }
    }
}

public class Y implements IY {
   
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void changeStatus(Object o, String status) {
        // We do a lot more here then just change this status because of inheriting objects but for the sake of the argument, change status
        o.status = status;
    }
}




This is a rough draft of what the code is doing.

I would expect the status change to be persisted because the outer transaction gets paused when the propogation_new transaction starts. I can also see the commit being called in the transaction code of Spring but for some reason it is not persisting to the database.

If I remove the throw of the runtime exception it works but the event completes which is not desired.

What am I missing in this picture? Hope you can help.

Thx!

EDIT

I think I found the problem, changed the sample code a little bit to make it more clear.

The changeStatus changes the status of the object that gets returned by the crudService. In the real application we do a lot more changes because of objects that rely on object o also need to change on status changes.

Because the outer transaction has a state of o, does that mean that if I make changes inside the inner transaction, because the outer transaction holds a reference, it will roll back to that state instead of persisting the changes of the inner transaction?

1
That code wouldn't compile: changeStatus is an instance method, not a static method. Is Y a spring bean? How does the t object get an instance of Y?JB Nizet
Yes Y is a spring bean. It gets injected through the constructor of x. Added constructor for clarification.MrM

1 Answers

0
votes

The problem is caused because the first transaction holds a reference to the object whose status is changed.

When we change the status in the new transaction, we commit that status change and return. When we return, the outer transaction resumes and throws a RuntimeException, which causes a rollback. Because the transaction holds a state of the object whose status was changed, that object gets rolled back to the state that the outer transaction has, which is the old status.

To fix this, instead of only having the status change in a new transaction, I moved all the logic into its own transaction and removed the transaction on the status change.

I then implemented a checked exception that gets thrown on status change that gets caught and then thrown to the parent. Every other exception gets caught and sends a RuntimeException to break.

The exceptions get caught by the parent and the service will throw a RuntimeException. Because the inner transaction finished and committed (in case of a checked exception) the status stays changed and the event fails for retry.

In my scenario I moved the logic to its own class/method, you can also leave the code in the same class but you will have to implement a proxy of itself and use that proxy to call so the Spring proxies through your method, otherwise it will ignore the transactional statement on the method.

Below is the final draft of what it looked like.

public class t implements it {
    // Do initialisation and class injection. Y is constructor injected
    private final B b;

    public t(B b) {
       this.b = b;
    }

    @Override
    @Transactional
    public void handleEvent(EventContext context) {
        switch (context.getEventType()) {
            case event:
                validate(context);
                break;
        }
    }

    // You can skip this method and simply call b, but in my scenario we do a couple of other things that do not have to be part of the transaction
    private void validate(EventContext context) {
        try {
            b.allLogicMethod(context.getObjectUuid());
        } catch(Exception e) {
            // Here we break the event so we can retry it, but the transaction succeeded in case it was a checked Exception
            throw new RuntimeException(e);
        }
    }
}

public class b implements IB {

    private final Y y;

    Public B(Y y) {
        this.Y = y;
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void allLogicMethod(String uuid) {
        try {
            Object o = crudService.findByProperty(context.getObjectUuid());
            if (!o.check) {
                y.changeStatus(o, ERROR);
                // break for retry
                throw new CheckedException("Some serious message log");
            } else {
                // do everything else
            }
        } catch(CheckedException ce) {
            throw ce;
        } catch(Exception e) {
            throw new RuntimeException("some message", e);
        }
    }
}

public class Y implements IY {

    @Override
    public void changeStatus(Object o, String status) {
        // We do a lot more here then just change this status because of inheriting objects but for the sake of the argument, change status
        o.status = status;
    }
}