1

There doesn't appear to be definite solution to concurrency problems in Grails (2.3.7). I've tried all the recommendations, but when I push the number of concurrent threads, the following piece of code invariably fails:

package simpledb

import grails.transaction.Transactional
import groovy.transform.Synchronized
import org.apache.commons.logging.LogFactory

@Transactional
class OwnerService {
    private static final myLock1 = new Object()
    private static final myLock2 = new Object()

    @Synchronized('myLock1')
    static public saveOwner(def ownerName) {
        def ownerInstance = null
        Owner.withNewTransaction {
            ownerInstance = Owner.findOrCreateByName(ownerName)
            ownerInstance.save(failOnError: true, flush: true)
        }
        ownerInstance
    }

    @Synchronized('myLock2')
    static public associateDog(def ownerId, def dogId) {
        def lockedOwnerInstance
        Owner.withNewTransaction {
            lockedOwnerInstance = Owner.lock(ownerId)
            def lockedDogInstance = Dog.lock(dogId)
            lockedOwnerInstance.addToDogs(lockedDogInstance)
            lockedOwnerInstance.save(failOnError: true, flush: true)
        }
        lockedOwnerInstance
    }
}

It fails on the line "def lockedDogInstance = Dog.lock(dogId)":

Error 500: Internal Server Error    

URI
      /simpledb/JsonSlurper/api
Class
      org.hibernate.StaleObjectStateException
Message
      Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [simpledb.Dog#111]

The above design is very simple where there's a Many-to-Many relationship between Owner and Dog:

Dog Class:

package simpledb

class Dog {
    String name
    Breed breed = null
    Integer age = null
    static hasMany = [owners: Owner]
    static belongsTo = Owner
    static mapping = { owners lazy: false }
    static constraints = {
        name blank: false, nullable: false, unique: true
        breed nullable: true
        age nullable: true
    }
}

Owner Class:

package simpledb

class Owner {
    String name;
    static hasMany = [dogs: Dog]
    static mapping = { dogs lazy: false }
    static constraints = {
    }
}

FYI - The DB is MySQL.

Any recommendations?

Tora Tora Tora
  • 973
  • 3
  • 18
  • 33

2 Answers2

1

OK, you've got a lot going on here, most of which I bet you can dispose of. So instead of trying to fix it, let's tear it down to the bare minimum and proceed from there:

  1. Your service methods should not be static.
  2. Your service is already transactional, so withNewTransaction() can go. You also don't need to flush.
  3. There's no need to synchronize the service methods.
  4. You don't need to lock on the Dog because you're not changing it (adding it to Owner.dogs only creates a record in the join table).

With these changes, your service ends up looking like this:

package simpledb

import grails.transaction.Transactional
import org.apache.commons.logging.LogFactory

@Transactional
class OwnerService {

    def saveOwner(def ownerName) {
        def ownerInstance = Owner.findOrCreateByName(ownerName)

        ownerInstance.save(failOnError: true)
        ownerInstance
    }

    def associateDog(def ownerId, def dogId) {
        def ownerInstance = Owner.lock(ownerId)
        def dogInstance = Dog.read(dogId)

        ownerInstance.addToDogs(dogInstance)
        ownerInstance.save(failOnError: true)
        ownerInstance
    }
}

See how far that takes you. You may even be able to remove the Owner lock.

Emmanuel Rosa
  • 9,697
  • 2
  • 14
  • 20
  • Thanks @Emmanuel-Rosa for your response. I made the changes you recommended (this is what I started with). However, I continue to get the same Error message as listed earlier. This time multiple threads fail on this error as compared to an occasional thread from my above listed code. Somehow, when it comes to concurrent DB modifications Grails fails to work as documented. – Tora Tora Tora Mar 02 '16 at 19:21
  • What is it about your app that makes it possible for multiple threads to attempt to update the same domain model? – Emmanuel Rosa Mar 02 '16 at 19:55
  • The application is essentially accepting REST requests with a JSON payload and persisting them in the database. However, before creating the record, we need to check if the data already exists, in which case the data needs to be updated. I'm using JMeter to test the code as it allows for simulating multi-thread calls. – Tora Tora Tora Mar 02 '16 at 20:01
  • But, if you're issuing a REST POST to create a record, and it fails because the record already exists, it should respond with a failure so that the client can turn around and issue a PUT to update it. And if the PUT fails because the record changed, then the client should re-attempt the PUT; up to n times and then finally give up. – Emmanuel Rosa Mar 02 '16 at 20:37
  • I should have elaborated further. There are multiple requests made simultaneously from different client. If the json payload contains a certain key that is NOT in the DB, then a row needs to be created and a reference to the row needs to be used to populate other tables. If the key already exists, then the reference is retrieved and other tables are populated. Hope that helps. – Tora Tora Tora Mar 03 '16 at 00:56
  • I've done some experimentation and arrived at some surprising results. Firstly, `lock()` uses a `SELECT ... FOR UPDATE` which in MySQL requires the InnoDB storage engine, so make sure you're using InnoDB (you probably already are). When using a many-to-many association, when you add a domain instance, such as in `ownerInstance.addToDogs(dogInstance)`, *both* domain instances get their version number increased: the owner *and* dog. In contrast, in a one-to-many only the *one* side gets the version number increase. This means you need to `lock()` on `dogInstance` too. – Emmanuel Rosa Mar 03 '16 at 02:54
  • You may need to get help in the Grails Slack community. Sorry, but I don't have any more troubleshooting ideas for you. http://slack-signup.grails.org/ – Emmanuel Rosa Mar 03 '16 at 23:02
0

Aside from what @Emmanuel-Rosa has said, if there are too many concurrent updates happening, can you also ensure to call 'refresh' (on owner) before saving? (repeatable reads approach).

Join table additions shouldn't suffer from these though. Only if some dogs are being attempted to be 're-added' to same owners, it could cause problem.

Another approach (not in this case, but) if only one or two columns are to be updated, you could use plain SQL.

Vishwajeet
  • 311
  • 3
  • 5