4

I'm using Grails 2.5.1, and I have a controller calling a service method which occasionally results in a StaleObjectStateException. The code in the service method has a try catch around the obj.save() call which just ignores the exception. However, whenever one of these conflicts occurs there's still an error printed in the log, and an error is returned to the client.

My GameController code:

def finish(String gameId) {
   def model = [:]
   Game game = gameService.findById(gameId)

   // some other work

   // this line is where the exception points to - NOT a line in GameService:
   model.game = GameSummaryView.fromGame(gameService.scoreGame(game))

   withFormat {
     json {
        render(model as JSON)
     }
   }
}

My GameService code:

Game scoreGame(Game game) {
   game.rounds.each { Round round ->
      // some other work
         try {
             scoreRound(round)
             if (round.save()) {
                 updated = true
             }
         } catch (StaleObjectStateException ignore) {
             // ignore and retry
         }
   }
}

The stack-trace says the exception generates from my GameController.finish method, it doesn't point to any code within my GameService.scoreGame method. This implies to me that Grails checks for staleness when a transaction is started, NOT when an object save/update is attempted?

I've come across this exception many times, and generally I fix it by not traversing the Object graph.

For example, in this case, I'd remove the game.rounds reference and replace it with:

def rounds = Round.findAllByGameId(game.id)
rounds.each {
   // ....
}

But that would mean that staleness isn't checked when the transaction is created, and it isn't always practical and in my opinion kind of defeats the purpose of Grails lazy collections. If I wanted to manage all the associations myself I would.

I've read the documentation regarding Pessimistic and Optimistic Locking, but my code follows the examples there.

I'd like to understand more about how/when Grails (GORM) checks for staleness and where to handle it?

Townsfolk
  • 1,282
  • 1
  • 9
  • 21

1 Answers1

3

You don't show or discuss any transaction configuration, but that's probably what's causing the confusion. Based on what you're seeing, I'm guessing that you have @Transactional annotations in your controller. I say that because if that's the case, a transaction starts there, and (assuming your service is transactional) the service method joins the current transaction.

In the service you call save() but you don't flush the session. That's better for performance, especially if there were another part of the workflow where you make other changes - you wouldn't want to push two or more sets of updates to each object when you can push all the changes at once. Since you don't flush, and since the transaction doesn't commit at the end of the method as it would if the controller hadn't started the transaction, the updates are only pushed when the controller method finishes and the transaction commits.

You'd be better off moving all of your transactional (and business) logic to the service and remove every trace of transactions from your controllers. Avoid "fixing" this by eagerly flushing unless you're willing to take the performance hit.

As for the staleness check - it's fairly simple. When Hibernate generates the SQL to make the changes, it's of the form UPDATE tablename SET col1=?, col2=?, ..., colN=? where id=? and version=?. The id will obviously match, but if the version has incremented, then the version part of the where clause won't match and the JDBC update count will be 0, not 1, and this is interpreted to mean that someone else made a change between your reading and updating the data.

Burt Beckwith
  • 75,342
  • 5
  • 143
  • 156
  • Hi Burt, thank you very much for the quick response. I actually don't use `@Transactional` in my `Controller`, only in my `Service`. I am also using `@GrailsCompileStatic` on my services, not sure if that would affect. I have some business logic in my controllers, but it's usually View related logic and I only update/modify objects from within my Services. Are there other transaction configuration I should be aware of? As it is now, all caching is turned off for Hibernate - query, and 2nd level. – Townsfolk Sep 21 '15 at 01:19
  • 1
    Ok, so it's not exactly how I described it, but similar. Since you don't flush in the method code, that only happens when the tx commits - the tx manager is Hibernate-aware and will flush the session before commit. So it happens after the method call, and there wouldn't be an exception inside, and it's only visible in the controller. So you should use explicit locking if you expect concurrent updates (this is a bit tricky when using collections), and explicit flushing if you want to catch the problem in the service method. – Burt Beckwith Sep 21 '15 at 01:24
  • As to when the staleness check is done - is the version check also done when lazily retrieving associations? As for the error only pointing to a line in my non-transactional Controller, is that an error in the stacktrace generation then? I would expect the top of the stacktrace printed in the logs to point to the line in the GameService where the round is saved. – Townsfolk Sep 21 '15 at 01:24
  • 2
    Calling `save()` to update a persistent instance (or `delete()`) is more of a message to Hibernate. Hibernate tries to avoid pushing to the database until it needs to, so it caches those changes in the session until the session is flushed. So the `save()` call won't be the trigger - it's the flush that pushes the changes and causes the database constraint checks, and the staleness check. But that staleness check is only done when updating, never when retrieving. – Burt Beckwith Sep 21 '15 at 01:27
  • Ah, I see now - It's throwing during the transaction commit which technically happens after my Service method is called so the error is appearing to generate from my Controller. Thank you for explaining that. I think explicit locking with some re-try logic will work best in my case. Thank you again! – Townsfolk Sep 21 '15 at 01:29