Search code examples
hibernategrailsgrails-ormquartz-scheduler

Catching RuntimeExceptions on grails transactions


Currently, we have a grails job that calls a transactional service. When an exception is thrown from the service, the behavior of hibernate becomes weird. We are using grails 2.4.4 and hibernate:3.6.10.18.

So inside my job I have this on the execute method:

Model.withTransaction { ->
    try {
        service.updateDatabase()
        service.method()//throws runtime exception
        } catch(RuntimeException e) {
            //do something
    }
}

The weird thing is, the updateDatabase operation does rollback. Looking at the logs, I can verify that it goes through in the catch block but still logs indicate that exception is still thrown. I thought that is why the transaction is rolling back.

But if I throw the RuntimeException directly on the job, it does NOT rollback the database transaction and the exception is cleanly caught. Under my impression, this should be the proper behavior, and it should be the same as throwing the exception from inside the service.

Model.withTransaction { ->
    try {
        service.updateDatabase()
        throw new RuntimeException()
        } catch(RuntimeException e) {
            //do something
    }
}

Is this normal? Is this a bug?


Solution

  • The expected behaviour is:

    Model.withTransaction { -> // Creates new Transaction
        try {
            service.updateDatabase() // uses the same Transaction that was created before
            service.method() // uses the same Transaction that was created before
                             // throws runtime exception
                             // sets Transaction as rollbackOnly
            } catch(RuntimeException e) {
                //do something
        }
    } // as the Transaction was set as rollbackOnly it rollbacks everything that was done before
    

    Basically that is the expected behaviour, now the explanation.

    All your service methods are Transactional because your service has a @Transactional above its name.

    @Transactional
    class MyTransactionalService {
    ...
    }
    

    And by default every Transactional method have the PROPAGATION.REQUIRED property set.

    /**
    * Support a current transaction, create a new one if none exists.
    * Analogous to EJB transaction attribute of the same name.
    * <p>This is the default setting of a transaction annotation.
    */
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)
    

    This means that when the service method runs it uses the current transaction that was created in your Job. Here comes the tricky part, when a functionality has more than one Transactional part it evaluates the rollback condition for every one of them, so when your method() throws RuntimeException it sets your transaction as rollbackOnly but continues until that Transaction is over (when your Model.withTransaction.. finishes) and just in that moment it rollbacks everything.

    Going deeper, you have three parts where your Transaction can be set to rollbackOnly.

    1. if updateDatabase() throws an Exception, all the Transaction will be set as rollbackOnly
    2. if method() throws an Exception, all the Transaction will be set as rollbackOnly
    3. if the closure that you pass to withTransaction{..} throws an Exception, all the Transaction will be set as rollbackOnly.

    The transaction will rollback when the Transaction is over, and that moment is after the withTransaction{..} finishes.

    So you need to be really careful with your Transactions.

    To solve your problem you can make your method() not Transactional by setting just the updateDatabase() as transactional in your Service class and deleting the @Transactional from above the service name.

    class YourService {
    
        @Transactional
        def updateDatabase() {
            //...
        }
    
        def method() {
            //...
        }
    }
    

    Setting just one method as @Transactional makes just that method as Transactional instead of having all the methods transactional.

    I made a project as an example so you can run the tests and check it for yourself, also I set the log4j to show the life of the Transaction so you can understand it better, you just need to run the MyProcessorIntegrationSpec.groovy

    https://github.com/juandiegoh/grails-transactions-rollback

    Hope it helps!