2

I've introduced a TransactionService that I use in my controllers to execute optimistic transactions. It should

  • try to execute a given transaction (= closure)
  • roll it back if it fails and
  • try it again if it fails

It basically looks like this:

class TransactionService {
  transactional = false // Because withTransaction is used below anyway
  def executeOptimisticTransaction(Closure transaction) {
    def success = false
    while (!success) {
      anyDomainClass.withTransaction { status ->
        try {
          transaction()
          success = true
        } catch(Exception e) {
          status.setRollbackOnly()
        }
      }
    }
  }
}

It is a little more complex, e.g. it uses different Thread.sleeps before trying again and aborts at some stage, but that doesn't matter here. It's called from controllers who pass the transaction to be safely executed as a closure.

My Problem: When the service hits a org.hibernate.StaleObjectStateException due to concurrent updates, it keeps trying again but the Exception never disappears.

I already tried different things like re-attaching domain classes in the transaction passed by the controller, clearing the session in the service or in the controller, but it didn't help. What am I missing?

I should note that I got an error that my "Transaction Manager does not allow nested transactions" when I tried to insert a savePoint before transaction() is called using status.createSavepoint(). I tried this because I also suspected that the error exists because the transaction is passed from the controller to the service and that I needed to start a new / nested transaction to avoid it, but as the error shows this is not possible in my case.

Or maybe is passing the transaction as a closure the problem?

I assume that the domain class used before the .withTransaction doesn't matter, or does it?

Jörg Brenninkmeyer
  • 3,304
  • 2
  • 35
  • 50
  • I don't understand why you're not using the regular approach where you have a service class that specifies transactional = true. That will make all methods in the service transactional. Can you clarify? – Hans Westerbeek Dec 07 '10 at 12:46
  • Because then I had to implement the "try again" functionality and everything else covered in this service in every controller - this stuff needs to be outside of the transaction. – Jörg Brenninkmeyer Dec 07 '10 at 12:55
  • i think you really need to explain why you need 'try again'. It's seems like a bit of an anti-pattern to me... – Hans Westerbeek Dec 07 '10 at 13:44
  • The Exception above is thrown when two users want to update certain data at the same time. When this happens, I don't want e.g. user 2 to see an error. Instead, the service should try again until the transaction of user 1 is finished so that the transaction of user 2 can be executed without an error. – Jörg Brenninkmeyer Dec 07 '10 at 14:01
  • i doubt that is really what you want, since the optimistic lock fails for a reason... i mean, you are basically trying to circumvent optimistic locking by forcing an overwrite. What you are programming will destroy the work of user 1 – Hans Westerbeek Dec 07 '10 at 14:28
  • No it doesn't - I give you an example: Domain classes Book, User, Rating. Transaction 1 of user 1 contains: a) Store Rating of User 1 for Book 1. b) Update Average Rating of Book 1 (persisted as Book.averageRating for performance reasons). Transaction 2 of user 2 is similar (also rating Book 1). Transaction 2 fails at b) because the average rating of Book 1 is being changed by user 1. Therefore a) of transaction 2 must be rolled back. Then the service tries again. Transaction 1 is finished, transaction 2 can be executed w/o errors / destroying anything. Hope this helps answering my question! – Jörg Brenninkmeyer Dec 07 '10 at 14:42

1 Answers1

1

It is not closure itself, but I believe transaction has some stale variable reference inside. What if you try to only pass closures that re-read their objects on execution? Like

executeOptimisticTransaction {
  Something some = Something.get(id)
  some.properties = aMap
  some.save()
}

I don't think it's possbile to "refresh" an object without re-reading it in Hibernate.

Yes, it doesn't matter what class you call .withTransaction() on.

For the example updating calculated totals/ratings, that's a data duplication that's a problem itself. I'd rather either:

  1. create a (Quartz) job that will update ratings based on some "dirty" flag - that might save some DB CPU for a cost of update time;
  2. or do it in SQL or HQL, like Book.executeQuery('update Rating set rating=xxx') that's going to use latest Rating. If you're optimizing for heavy load, you're anyway not going to do everything Groovy-way. Don't save Rating objects in Grails, only read them.
Victor Sergienko
  • 13,115
  • 3
  • 57
  • 91
  • I thought I had checked it, but you're obviously right! It was sufficient to do a new .get on the domain objects I needed within the closure. However it still seems to me as if the transactions would not be executed "atomically" - could it be that .withTransaction doesn't stop transactions from being mixed up? – Jörg Brenninkmeyer Dec 07 '10 at 16:30
  • It's impossible for transaction not to be atomic. There must be some other source of wonders :) What makes you think so? Or the service could be transactional, and nested transactions would be just no-op. – Victor Sergienko Dec 07 '10 at 17:39
  • Yes the passed closure sometimes calls other transactional services. Are you saying that these parts then run as separate transactions, so that nested transactions of different services could be mixed up? In this case, I guess I should set the other services to "transactional = false"!? P.S. What does no-op mean? ;-) – Jörg Brenninkmeyer Dec 07 '10 at 17:59
  • I mean that inside a single transactional call (i.e. transactional service method) all other withTransaction() calls will have no effect. So if you had some enclosing transactional call, you could experience weird behavior of transactions. No-op is "no operation", empty statement. – Victor Sergienko Dec 07 '10 at 18:04
  • Ok, I checked that but it didn't help setting the used services to transactional = false. The reason why I think the transactions are not atomic is that the outcomes are different (and wrong) when I run them at the same time. When I run the same transactions subsequently it works, and the order of execution doesn't matter in my case. There are several queries and updates mixed within one transaction, but with a clean roll-back upon errors and an atomic execution, the results should be the same, shouldn't they? – Jörg Brenninkmeyer Dec 07 '10 at 18:57
  • 1
    Do you take into account that one transaction doesn't 'see' other's changes if the other didn't commit by 1st's beginning? So simultaneous transactions will use same data, not updated by each other, which will result in wrong average Rating. So the idea of separate Rating updater Job has some advantage. – Victor Sergienko Dec 08 '10 at 09:47
  • I think I know what the problem was. I expected DB queries within a transaction to be executed atomically as well. But I guess transaction 1 can be executed between queries in transaction 2, even if the data modifications in transaction 2 are only done atomically after that. So transaction 2 got inconsistent query results as the basis for their data modifications. Can anybody confirm this? – Jörg Brenninkmeyer Dec 08 '10 at 09:47
  • Wow, this was also a concurrent modification! ;-) You got it. For the average calculation I think your approach is the better one, however the transaction causing problems in my case is of another kind. I introduced a "manual" lock now that also protects queries from getting inconsistent data (causing my new Question http://stackoverflow.com/questions/4375757/how-to-overcome-staleobjectstateexception-in-grails-service ;-). Thank you very much for your help! – Jörg Brenninkmeyer Dec 08 '10 at 09:50
  • Just realized that your comment contradicts my expectation - are you really sure that when transaction 1 is executed atomically, queries happening in transaction 2 still see the state before the execution of transaction 1 until transaction 2 is finished? It didn't seem so in my experience I just gained. – Jörg Brenninkmeyer Dec 08 '10 at 09:57
  • Yes, unless your DB is working on lower level of transaction isolation, or there is no transaction at all. The first case is unlikely, as the outcome won't be reproducible. An example would help, it's probably a base of another question :) I was also surprised, but as long as it gave you a helping idea... – Victor Sergienko Dec 08 '10 at 10:01
  • Regarding the DB, I have a mySQL DB connected via the (almost) latest JDBC driver, so I also think this isn't the problem. But how could there be no transaction when I use .withTransaction? – Jörg Brenninkmeyer Dec 08 '10 at 10:02
  • Just checked the mySQL manual. It differentiates "START TRANSACTION" and "START TRANSACTION WITH CONSISTENT SNAPSHOT" (see http://dev.mysql.com/doc/refman/5.0/en/commit.html). the "WITH CONSISTENT SNAPSHOT" seems to be the one where queries within a transaction see the DB before the transaction started (except for own changes). So in your opinion, this is used by default and not only "START TRANSACTION"? – Jörg Brenninkmeyer Dec 08 '10 at 10:09
  • No, I think it's the wrong way. I always find the source for seemingly-impossible things in my own code, so I'd recommend looking at your code. – Victor Sergienko Dec 08 '10 at 10:11