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:
- Retrieve the entity.
- Refresh it to reduce the chances of an optimistic lock failure.
- Make changes and commit them.
- 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:
- Wrap the
pm.refresh
call in anif (!JDOHelper.isDirty(toUpdate))
check, to ensurepm.refresh
is never called on a dirty entity. - Before the
pm.refresh
call, if the retrieved entity is dirty, callEntityManager#detach
on it and then callpm.getObjectById
again to get a clean one. - After catching
JDOOptimisticVerificationException
, useJDOHelper.getVersion(toUpdate)
to compare the version before and afterpm.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!