3
votes

Is optimistic locking supposed to catch concurrent update issues?

By concurrent update, I mean that two different users both attempt to update an object with the same version number. For example, if there is a Person domain class, and Person with id = 1, and version = 0, has name = Jack, and two different users both attempt to update the name on version 0, I would expect only one of the user to succeed and change the version to 1. I would expect the second user to get a Hibernate staleStateException or something similar. But that's not what happens.

Here's my use case:

Grails 3.1.5
grails generate-app person
grails create-domain-class Person
Edit Person.groovy to include String name
grails generate-all person.Person
gradle bootRun

Use two different browsers to access the app, such as Chrome and Firefox, to ensure that the two browsers are in different sessions. Create a Person in one of them and then open that same person (version 0) for editing in both browsers. Both browsers should now be editing version 0 of person. Save a name change in one browser, this works and changes the persisted version of the object to 1, but the second browser is still editing version 0. Now save the changes in the second browser, this also works. Despite the fact that the second browser just saved changes to a now stale object (version 0) no StaleObject or StaleStateException is thrown. Is this the correct behavior?

2

2 Answers

1
votes

Yes, Grails optimistic locking will detect a concurrent update and throw an exception. However, based on what you described, you're not doing a concurrent update. Let's take a closer look.

  1. The starting point is with both browsers having retrieved the same version of an object. Note however, that the browsers do not hold a reference to these objects. The GSP code grabbed them from the database and rendered them for the browser to display. In other words, the same object is being viewed not edited.
  2. When the first browser calls upon the controller to save the changes, the controller retrieves a fresh copy of the object from the database: ex. DomainClass.get(params.id). Then the changes are saved.
  3. When the second browser calls upon the controller to save the changes, the controller once again retrieves a fresh copy of the object from the database. This time it's version 1, but that's irrelevant because the version has nothing to do with retrieving the object, only when saving. And so the second save succeeds because the version matches what's already in the database.

To create the condition you're seeking, you'd have to play with it until there's an overlap long enough between both saves so that a sequence like this happens:

def a = DomainClass.get(1)
def b = DomainClass.get(1)

/* change a */
a.save()

/* change b */
b.save() // This would throw an exception because b.version does not match what's in the database.
-1
votes

I think the precise answer is Grail doesn't implement optimistic lock between requests out of the box.

The confusion comes from the fact that scaffolding generates some code necessary (but not sufficient) for that in edit.gsp:

<g:hiddenField name="version" value="${myInstance?.version}" />

You must implement an extra check in your controller save() action to compare the original rendered version of your record with the database version in the exact saving instant, and inform the user that a dirty read occurred during concurrent edit operations. Something like:

if (new Long(params.version) != myInstance.version) {
    myInstance.errors.rejectValue("", "", "This record has been updated by a concurrent user. Please reopen it before saving again");
    myInstance.version = new Long(params.version); //the version to be re-rendered must still be the original one, just in case the user try saving again without refreshing
    return render(view: "edit", model: [myInstance: myInstance]);
}