0
votes

Grails 2.5.5 transaction issue.

We have a get Balance call API call which is a simple controller which calls a few services. It does 2 things, returns a customers accounts with their balance and updates the customers session (like a keep alive).

The problem is when the client calls getBalance twice at the same time, instead one always throws an exception, even though in the save() we have failOnError:false (and putting try catch around does not help, see below). We need the refreshSession to silently fail if someone else is currently refreshing the session anyway, and return the account balances as if nothing went wrong. We cant figure out how to do this in grails as failOnError:false and try catch are not working, and neither discard nor refresh has any effect.

SessionService:

boolean refreshSession( Session aSession ) {
    if ( aSession != null ) {
        aSession.lastUpdatedAt = new Date()
        aSession.save(flush:true, failOnError: false)  // line 569.  This always fails on error even with failOnError set to false.
        return true
    }
    return false
}

This is the error we get:

org.springframework.orm.hibernate4.HibernateOptimisticLockingFailureException: Object of class [com.xxx.Session] with identifier [23]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.xxx.Session#23]
at com.xxx.services.SessionService$$EQ2LbJCm.refreshSession(SessionService.groovy:569)
at com.xxx.getBalance(AccountController.groovy:56)

How can it fail if failOnError = false?

The session service is beeing called by a controller which has no transactional properities defined:

Controller:

def getBalance() {
 try {
        :
        Session session = sessionService.getSession(payload.token)

        sessionService.refreshSession(session) // this updates the session object

        def accountResult = accountService.getBalance(session.player)  // line 59: this returns a structure of account domain objects 
                                                                       // which have a balance and currency etc. It is ready only!

        render(status: 200, contentType: 'application/json') {
            [
                   'result'  : 0,
                   'accounts': accountResult.accounts
            }
        return

     } catch (Exception e) {
        renderError(ERROR, e.toString())
        log.error("getBalance API Error:" + e.toString(), e)
    }

What we have tried:

  1. making refreshSession Transactional and NonTransactional (same result)
  2. removed "flush:true" from refreshSession. (same result)
  3. adding try catch (Exception e) around body of refresh session. This did not catch the exception. (same result)
  4. adding try catch (Exception e) around body of refresh session AND making refreshSession NotTransactional.

Interestingly, the last one changed the line the exception occurs on to the next line (the one to read the accounts, which does not writing, only reading)

org.springframework.orm.hibernate4.HibernateOptimisticLockingFailureException: Object of class [com.xxx.Session] with identifier [23]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.xxx.Session#23]
at com.xxx.getBalance(AccountController.groovy:59)
  1. Tried discard(). Did not help.

     @NotTransactional
    
     boolean refreshSession( Session aSession ) {
        if (aSession != null) {
            try {
                aSession.lastUpdatedAt = new Date()
                aSession.save(failOnError: false)
            } catch (Exception e) {
                aSession.discard()
                aSession.refresh()
            } finally {
                return true
            }
        }
    
     return false
     }
    
  2. Tried the above with flush:true. Same result.

  3. Tried making refreshSession get its own copy of the session in its own transaction.

    @Transactional
    boolean refreshSession( long id ) {
        Session session = Session.get(id)
        if (session != null) {
            try {
                session.lastUpdatedAt = new Date()
                session.save(flush: true, failOnError: false)
           } catch (Exception e) {
               println("XXX got exception:" + e.message)
               session.discard()
           } finally {
               return true
           }
        }
        return false
    }
    

This fails with the original exception. It seems not possible to ignore a failed write in grails. Bazarely, even though the exception is caught, and it prints "XXX got exception", the exception is thrown again for no apparent reason.

My understanding of making everything NonTransaction is it works like having autoCommit:true - every db update will happen immediately, and if one thing fails, it wont rollback the other things?

Any Ideas?

2

2 Answers

3
votes

How can it fail if failOnError = false?

You mentioned HibernateOptimisticLockingFailureException. failOnError doesn't have anything to do with that. failOnError controls whether or not an exception is thrown when validation errors occur during .save(). That would not affect HibernateOptimisticLockingFailureException.

1
votes

I think you need .withNewTransaction

boolean refreshSession( long id ) {
Session.withNewTransaction {
Session session = Session.get(id)
    if (session != null) {
        try {
            session.lastUpdatedAt = new Date()
            session.save(flush: true, failOnError: false)
       } catch (Exception e) {
           println("XXX got exception:" + e.message)
           session.discard()
       } finally {
           return true
       }
    }
    }
    return false
}

In the link provided, it was returning the latest status since the element may have been updated during the process whilst it got record and was attempting to check it. You could also look into isDirty as well as getPersistentValue. Personally think wrapping withNewTransaction will resolve the issue. You say you want to discard 2nd click but actually isn't that valid and latest lastUpdatedAt ?