Refreshing a dirty object causes permanent JDOOptimisticVerificationException

#1

Hi, I'm not sure if I'm doing something wrong or if there's a bug. I made a minimal reproducible example:

package test;
import com.objectdb.Enhancer;
import javax.jdo.JDOHelper;
import javax.jdo.JDOOptimisticVerificationException;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.persistence.Entity;public class OdbTest {

public static class Enhance {

public static void main(String[] args) {
Enhancer.enhance(TestEntity.class.getName());
}
}

@Entity
public static class TestEntity {

private String value;

public void setValue(String newValue) {
this.value = newValue;
}
}

public static class InfiniteLoop {

public static void main(String[] args) {
PersistenceManagerFactory pmf = JDOHelper.getPersistenceManagerFactory("objectdb:test.mem");
PersistenceManager pm = pmf.getPersistenceManager();

Object oid = commitTestEntity(pm);
retrieveEntityAndMakeItDirtyByAccident(pm, oid);
tryToUpdate(pm, oid);
}

private static Object commitTestEntity(PersistenceManager pm) {
pm.currentTransaction().begin();

TestEntity testEntity = new TestEntity();
pm.makePersistent(testEntity);
Object oid = pm.getObjectId(testEntity);

pm.currentTransaction().commit();
return oid;
}

private static void retrieveEntityAndMakeItDirtyByAccident(PersistenceManager pm, Object oid) {
// The entity is retrieved and accidentally written to, which causes it to become NonTransactional-Dirty.
TestEntity accidentalWrite = (TestEntity) pm.getObjectById(oid, true);
accidentalWrite.setValue("a");
}

private static void tryToUpdate(PersistenceManager pm, Object oid) {
// The entity is retrieved, with the intention of modifying it and committing it.
TestEntity toUpdate = (TestEntity) pm.getObjectById(oid, true);

// Refresh the entity to ensure we have the latest version, to reduce the chance of an optimistic lock failure.
pm.refresh(toUpdate);

// Try to make changes to the entity and commit them.
while (true) {
try {
pm.currentTransaction().begin();
toUpdate.setValue("b");
pm.currentTransaction().commit();
break;
} catch (JDOOptimisticVerificationException e) {
System.out.println(e.getMessage());

// In case of an optimistic lock failure, get the latest version, and loop back to redo the changes and re-commit.
toUpdate = (TestEntity) pm.getObjectById(oid, true);
}
}
}
}
}

In the tryToUpdate method, my goal is to:

  1. Retrieve the entity.
  2. Refresh it to reduce the chances of an optimistic lock failure.
  3. Make changes and commit them.
  4. Handle JDOOptimisticVerificationException by retrieving the entity again, and redoing step 3.

This generally works and survives a stress test where many threads are trying to modify the entity at the same time, but I found an edge case which causes JDOOptimisticVerificationException to always be thrown, causing an infinite loop of the same log message:

  • "Optimistic lock failed for object test.OdbTest$TestEntity#1 (object has version 1 instead of 2) [TestEntity:1]"

For me, this is always reproducible in the example I posted, by first running OdbTest.Enhance.main, then OdbTest.InfiniteLoop.main. We have been using ObjectDB 2.8.9.b04, but I verified it's still reproducible in 2.9.4.

There are two requirements for the issue to happen:

  • The entity must be made dirty on accident, outside of a transaction (retrieveEntityAndMakeItDirtyByAccident method).
  • The dirty entity must be refreshed (beginning of the tryToUpdate method).

If I comment out either the retrieveEntityAndMakeItDirtyByAccident(pm, oid); or the pm.refresh(toUpdate); line, the commit is successful.

In my case, making the entity dirty by accident is a bug that I need to fix (for example by making the entity transactional before passing it to potentially buggy code), but what I don't understand is why refreshing the dirty entity makes it impossible to commit again, due to commit always throwing JDOOptimisticVerificationException even if I try to obtain the latest version of the entity.

I found a couple possible mitigations:

  1. Wrap the pm.refresh call in an if (!JDOHelper.isDirty(toUpdate)) check, to ensure pm.refresh is never called on a dirty entity.
  2. Before the pm.refresh call, if the retrieved entity is dirty, call EntityManager#detach on it and then call pm.getObjectById again to get a clean one.
  3. After catching JDOOptimisticVerificationException, use JDOHelper.getVersion(toUpdate) to compare the version before and after pm.getObjectById, and if the version is the same, exit out of the loop because the assumption that there is a newer version in the database was wrong.

However, because I don't understand the root cause of the issue, I don't know if any of these are a proper solution, and that there isn't another edge case that would cause the entity to become impossible to commit.

Option 2 with EntityManager#detach sounds robust to me, however I'm using a PersistenceManager and not an EntityManager, and I can't find any equivalent of EM.detach that would do the same; PM.detachCopy doesn't work. I found that ObjectDB's implementation of PersistenceManager is also an EntityManager, so I could cast it, but that feels like relying on an implementation detail.

Any help with understanding what's happening here would be appreciated. Thanks!

#2

Please try version 2.9.4_01, which should fix the issue.

Note that this bug affects only the refresh method in JDO (not JPA).

We highly recommend switching to JPA, as it is more modern and better maintained.

Support for JDO in ObjectDB is approaching end of life.

ObjectDB Support

Reply