78

I have this scenario:

  1. fetch (read and delete) a record from IncomingMessage table
  2. read record content
  3. insert something to some tables
  4. if an error (any exception) occurred in steps 1-3, insert an error-record to OutgoingMessage table
  5. otherwise, insert an success-record to OutgoingMessage table

So steps 1,2,3,4 should be in a transaction, or steps 1,2,3,5

My process starts from here (it is a scheduled task):

public class ReceiveMessagesJob implements ScheduledJob {
// ...
    @Override
    public void run() {
        try {
            processMessageMediator.processNextRegistrationMessage();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
// ...
}

My main function (processNextRegistrationMessage) in ProcessMessageMediator:

public class ProcessMessageMediatorImpl implements ProcessMessageMediator {
// ...
    @Override
    @Transactional
    public void processNextRegistrationMessage() throws ProcessIncomingMessageException {
        String refrenceId = null;
        MessageTypeEnum registrationMessageType = MessageTypeEnum.REGISTRATION;
        try {
            String messageContent = incomingMessageService.fetchNextMessageContent(registrationMessageType);
            if (messageContent == null) {
                return;
            }
            IncomingXmlModel incomingXmlModel = incomingXmlDeserializer.fromXml(messageContent);
            refrenceId = incomingXmlModel.getRefrenceId();
            if (!StringUtil.hasText(refrenceId)) {
                throw new ProcessIncomingMessageException(
                        "Can not proceed processing incoming-message. refrence-code field is null.");
            }
            sqlCommandHandlerService.persist(incomingXmlModel);
        } catch (Exception e) {
            if (e instanceof ProcessIncomingMessageException) {
                throw (ProcessIncomingMessageException) e;
            }
            e.printStackTrace();
            // send error outgoing-message
            OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId,
                    ProcessResultStateEnum.FAILED.getCode(), e.getMessage());
            saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
            return;
        }
        // send success outgoing-message
        OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId, ProcessResultStateEnum.SUCCEED.getCode());
        saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
    }

    private void saveOutgoingMessage(OutgoingXmlModel outgoingXmlModel, MessageTypeEnum messageType)
            throws ProcessIncomingMessageException {
        String xml = outgoingXmlSerializer.toXml(outgoingXmlModel, messageType);
        OutgoingMessageEntity entity = new OutgoingMessageEntity(messageType.getCode(), new Date());
        try {
            outgoingMessageService.save(entity, xml);
        } catch (SaveOutgoingMessageException e) {
            throw new ProcessIncomingMessageException("Can not proceed processing incoming-message.", e);
        }
    }
// ...
}

As i said If any exception occurred in steps 1-3, i want insert an error-record:

catch (Exception e) {
    if (e instanceof ProcessIncomingMessageException) {
        throw (ProcessIncomingMessageException) e;
    }
    e.printStackTrace();
    //send error outgoing-message
    OutgoingXmlModel outgoingXmlModel = new OutgoingXmlModel(refrenceId,ProcessResultStateEnum.FAILED.getCode(), e.getMessage());
    saveOutgoingMessage(outgoingXmlModel, registrationMessageType);
    return;
}

It's SqlCommandHandlerServiceImpl.persist() method:

public class SqlCommandHandlerServiceImpl implements SqlCommandHandlerService {
// ...
    @Override
    @Transactional
    public void persist(IncomingXmlModel incomingXmlModel) {
        Collections.sort(incomingXmlModel.getTables());
        List<ParametricQuery> queries = generateSqlQueries(incomingXmlModel.getTables());
        for (ParametricQuery query : queries) {
            queryExecuter.executeQuery(query);
        }
    }
// ...
}

But when sqlCommandHandlerService.persist() throws exception (here a org.hibernate.exception.ConstraintViolationException exception), after inserting an error-record in OutgoingMessage table, when the transaction want to be committed , i get UnexpectedRollbackException. I can't figure out where is my problem:

Exception in thread "null#0" org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:717)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:394)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:120)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:622)
    at x.y.z.ReceiveMessagesJob$$EnhancerByCGLIB$$63524c6b.run(<generated>)
    at x.y.z.JobScheduler$ScheduledJobThread.run(JobScheduler.java:132)

I'm using hibernate-4.1.0-Final, My database is oracle, and Here is my transaction-manager bean:

<bean id="transactionManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<tx:annotation-driven transaction-manager="transactionManager"
    proxy-target-class="true" />
Arya
  • 2,809
  • 5
  • 34
  • 56
  • paste complete stackTrace – ankit Oct 14 '13 at 06:04
  • I noticed that this exception throwned when repository itself can't do job, not other parts of your code. – lalilulelo_1986 May 13 '21 at 18:11
  • In most cases, this exception is just a summary. If you check your Spring output logs, you'll see some inner exceptions which make the transaction be roll-backed. For example, your entity stores some transient entity and your cascade rules don't handle such a case. – Dmytro Moskovchenko Feb 01 '23 at 11:23

2 Answers2

76

This is the normal behavior and the reason is that your sqlCommandHandlerService.persist method needs a TX when being executed (because it is marked with @Transactional annotation). But when it is called inside processNextRegistrationMessage, because there is a TX available, the container doesn't create a new one and uses existing TX. So if any exception occurs in sqlCommandHandlerService.persist method, it causes TX to be set to rollBackOnly (even if you catch the exception in the caller and ignore it).

To overcome this you can use propagation levels for transactions. Have a look at this to find out which propagation best suits your requirements.

Update; Read this!

Well after a colleague came to me with a couple of questions about a similar situation, I feel this needs a bit of clarification.
Although propagations solve such issues, you should be VERY careful about using them and do not use them unless you ABSOLUTELY understand what they mean and how they work. You may end up persisting some data and rolling back some others where you don't expect them to work that way and things can go horribly wrong.


EDIT Link to current version of the documentation
shyam
  • 6,681
  • 7
  • 28
  • 45
Ean V
  • 5,091
  • 5
  • 31
  • 39
  • Thanks Ean. I'm not sure using REQUIRES_NEW or NESTED!? (I have tested REQUIRES_NEW and it worked) – Arya Oct 14 '13 at 16:28
  • 5
    The difference between these two is that, NESTED is like a child TX so if parent gets committed it will get committed otherwise not. REQUIRES_NEW creates a new TX and suspends the existing one. So it gets committed individually. Be careful about your container as these are not supported in all containers. The link above has some explanation about the environments that support these types of propagations. – Ean V Oct 14 '13 at 22:52
  • if use spring-test and `defaultRolback` is false(@TransactionConfiguration(defaultRollback = false)), you also could have this exception. To resolve it just need set defaultRollback = true – zhuguowei Jun 18 '16 at 10:31
  • Which method is it suggested to add this Propagation configuration to the @Transactional annotation? SqlCommandHandlerServiceImpl.persist() (and thus require overriding), or processNextRegistrationMessage()? Does the separate transaction propagation requirement apply to the transactional method that catches or throws the exception? – acidnbass Jan 24 '17 at 20:04
  • @acidnbass both. They will have separate TXs after adding the annotations. – Ean V Jan 24 '17 at 21:48
  • the issue illustration with simple code can be found at https://stackoverflow.com/a/19312020/355438 – Ilya Serbis Dec 10 '20 at 01:07
31

The answer of Shyam was right. I already faced with this issue before. It's not a problem, it's a SPRING feature. "Transaction rolled back because it has been marked as rollback-only" is acceptable.

Conclusion

  • USE REQUIRES_NEW if you want to commit what did you do before exception (Local commit)
  • USE REQUIRED if you want to commit only when all processes are done (Global commit) And you just need to ignore "Transaction rolled back because it has been marked as rollback-only" exception. But you need to try-catch out side the caller processNextRegistrationMessage() to have a meaning log.

Let's me explain more detail:

Question: How many Transaction we have? Answer: Only one

Because you config the PROPAGATION is PROPAGATION_REQUIRED so that the @Transaction persist() is using the same transaction with the caller-processNextRegistrationMessage(). Actually, when we get an exception, the Spring will set rollBackOnly for the TransactionManager so the Spring will rollback just only one Transaction.

Question: But we have a try-catch outside (), why does it happen this exception? Answer Because of unique Transaction

  1. When persist() method has an exception
  2. Go to the catch outside

    Spring will set the rollBackOnly to true -> it determine we must 
    rollback the caller (processNextRegistrationMessage) also.
    
  3. The persist() will rollback itself first.

  4. Throw an UnexpectedRollbackException to inform that, we need to rollback the caller also.
  5. The try-catch in run() will catch UnexpectedRollbackException and print the stack trace

Question: Why we change PROPAGATION to REQUIRES_NEW, it works?

Answer: Because now the processNextRegistrationMessage() and persist() are in the different transaction so that they only rollback their transaction.

Thanks

chubbsondubs
  • 37,646
  • 24
  • 106
  • 138