Heath Thomann created OPENJPA-2476:
--------------------------------------

             Summary: OptimisticLockEx due to rounding of a Timestamp (either 
by OJ, or the DB)
                 Key: OPENJPA-2476
                 URL: https://issues.apache.org/jira/browse/OPENJPA-2476
             Project: OpenJPA
          Issue Type: Bug
          Components: jdbc
    Affects Versions: 2.2.0, 2.1.0, 2.2.1.1, 2.3.0, 2.4.0
            Reporter: Heath Thomann
            Assignee: Heath Thomann
            Priority: Minor


HI!  I'm hitting a rather interesting issue and unfortunately to describe it is 
going to be a bit lengthy......so settle in.  :)  Here is the exception:

<openjpa-2.1.2-SNAPSHOT-r422266:1548248 fatal store error> 
org.apache.openjpa.persistence.RollbackException: Optimistic locking errors 
were detected when flushing to the data store.  The following objects may have 
been concurrently modified in another transaction: 
[hat.entities.VersionTSEntity-1390400526251]
        at 
org.apache.openjpa.persistence.EntityManagerImpl.commit(EntityManagerImpl.java:593)
        at hat.tests.PMR57956Test.testupdate(PMR57956Test.java:94)
........
Caused by: <openjpa-2.1.2-SNAPSHOT-r422266:1548248 nonfatal store error> 
org.apache.openjpa.persistence.OptimisticLockException: Optimistic locking 
errors were detected when flushing to the data store.  The following objects 
may have been concurrently modified in another transaction: 
[hat.entities.VersionTSEntity-1390400526251]
        at 
org.apache.openjpa.kernel.BrokerImpl.newFlushException(BrokerImpl.java:2310)
        at org.apache.openjpa.kernel.BrokerImpl.flush(BrokerImpl.java:2158)
        at org.apache.openjpa.kernel.BrokerImpl.flushSafe(BrokerImpl.java:2056)
        at 
org.apache.openjpa.kernel.BrokerImpl.beforeCompletion(BrokerImpl.java:1974)
        at 
org.apache.openjpa.kernel.LocalManagedRuntime.commit(LocalManagedRuntime.java:81)
        at org.apache.openjpa.kernel.BrokerImpl.commit(BrokerImpl.java:1498)
        at 
org.apache.openjpa.kernel.DelegatingBroker.commit(DelegatingBroker.java:933)
        at 
org.apache.openjpa.persistence.EntityManagerImpl.commit(EntityManagerImpl.java:569)
        ... 21 more
Caused by: <openjpa-2.1.2-SNAPSHOT-r422266:1548248 nonfatal store error> 
org.apache.openjpa.persistence.OptimisticLockException: An optimistic lock 
violation was detected when flushing object instance 
"hat.entities.VersionTSEntity-1390400526251" to the data store.  This indicates 
that the object was concurrently modified in another transaction.
FailedObject: hat.entities.VersionTSEntity-1390400526251
        at 
org.apache.openjpa.jdbc.kernel.AbstractUpdateManager.populateRowManager(AbstractUpdateManager.java:183)
        at 
org.apache.openjpa.jdbc.kernel.AbstractUpdateManager.flush(AbstractUpdateManager.java:97)
        at 
org.apache.openjpa.jdbc.kernel.AbstractUpdateManager.flush(AbstractUpdateManager.java:78)
        at 
org.apache.openjpa.jdbc.kernel.JDBCStoreManager.flush(JDBCStoreManager.java:742)
        at 
org.apache.openjpa.kernel.DelegatingStoreManager.flush(DelegatingStoreManager.java:131)
        ... 28 more


To see how this occurs, lets look at the test.  Here is the entity:

@Entity
public class VersionTSEntity implements Serializable {
    @Id
    private Long id;
    
    @Version
    private Timestamp updateTimestamp;

    private Integer someInt;


And here is my test, with pertinent in-line comments:

//This loop is necessary since we need a timestamp which has been //rounded up 
by either OJ, or the database....usually 1 or 2 loops will cause //the 
exception.
        for (int i = 0; i < 1000; i++) {

            // Find an existing VersionTSEntity:
            VersionTSEntity t = em.find(VersionTSEntity.class, id);

            tx.begin();
            // Update/dirty VersionTSEntity:   
            t.setSomeInt(t.getSomeInt() + 1);
            t = em.merge(t);

            tx.commit();
            //If this clear is removed the test works fine.
            em.clear();

// Lets say at this point the 'in-memory' timestamp is: 
// 2014-01-22 07:22:11.548778567.  What we actually sent to the DB (via 
// the previous merge) is by default rounded (see 
// DBDictionary.setTimestamp) to the nearest millisecond on Oracle (see 
// DBDictionary.datePrecision) and nearest microsecond on DB2 (see 
// DB2Dictionary.datePrecision) when sending the value to the db.
// Therefore, what we actually send to the db is: 
// 2014-01-22 07:22:11.548779 (for DB2) or 2014-01-22 07:22:11.549 (for 
// Oracle).  
// Notice in either case we rounded up...keep this in mind as it will be 
// important for the remainder of the test......

            // now, do a merge with the unchanged entity
            tx = em.getTransaction();
            tx.begin();

            t = em.merge(t); 
            
            //This 'fixes' the issue:
            //em.refresh(t);
            
// Here is where things get interesting.....an error will happen here when 
// the timestamp has been rounded up, as I'll explain:
// As part of this merge/commit, we select the timestamp from the db to get 
// its value (see method checkVersion below), i.e: 
// 'SELECT t0.updateTimestamp FROM VersionTSEntity t0 WHERE 
//     t0.id = ?'.  
// We then compare the 'in-memory' timestamp to that which we got back 
// from the DB, i.e. on DB2 we compare:
// in-mem:  2014-01-22 07:22:11.548778567
// from db: 2014-01-22 07:22:11.548779
// Because these do not 'compare' properly (the db version is greater), we 
// throw the OptimisticLockEx!!
// For completeness, lets look at an example where the timestamp is as 
// follows after the above update: 2014-01-22 07:22:11.548771234.  We 
// would send to DB2 the following value: 2014-01-22 07:22:11.548771.  
// Then, as part of the very last merge/commit, we'd compare:
// in-mem:  2014-01-22 07:22:11.548771234
// from db: 2014-01-22 07:22:11.548771
// These two would 'compare' properly (the db version is lesser), as such 
// we would not throw an OptLockEx and the test works fine.
   
         tx.commit();


To see where/why the OptLockEx is thrown, we need to look here in 
VersionColumnStrategy (see //HAT:):

public boolean checkVersion(OpenJPAStateManager sm, JDBCStore store,
        boolean updateVersion)  throws SQLException {
        Column[] cols = vers.getColumns();
        Select sel = store.getSQLFactory().newSelect();
        sel.select(cols);
        sel.wherePrimaryKey(sm.getObjectId(), vers.getClassMapping(), 
                                            store);

        // No need to lock version field (i.e. optimistic), except when version 
update is required (e.g. refresh) 
        JDBCFetchConfiguration fetch = store.getFetchConfiguration();
        if (!updateVersion && fetch.getReadLockLevel() >= 
MixedLockLevels.LOCK_PESSIMISTIC_READ) {
            fetch = (JDBCFetchConfiguration) fetch.clone();
            fetch.setReadLockLevel(MixedLockLevels.LOCK_NONE);
        }
        Result res = sel.execute(store, fetch);
        try {
            if (!res.next())
                return false;

            //HAT: here is where we get the timestamp from the db, and 
'compare' them:
            Object memVersion = sm.getVersion();
            Object dbVersion  = populateFromResult(res, null);
            
            boolean refresh   = compare(memVersion, dbVersion) < 0;

            if (updateVersion)
                sm.setVersion(dbVersion);
            return !refresh;
        } finally {
            res.close();
        }
    }



Keep in mind here that it seems like the scenario is a bit unique....in other 
words, if I remove the 'em.clear', we do not deem it necessary to call the 
'checkVersion' method (thus we don't go back to the DB to get the timestamp).  
So it seems that there are two unique things causing us to hit the issue: 1) 
merge/clear/merge which causes a 'checkVersion', 2) rounding of timestamps.  I 
added the 'clear' in my test because it is at this point the customer returns 
the entity to the caller of an EJB....the caller then sends the entity back to 
the EJB to merge it.....

Now, there are two existing fixes to this issue on DB2, and one fix for this on 
Oracle.  As you can see in my test above, if we 'em.refresh' the entity before 
commit, the in-memory value will match that of the db value.  This fix works 
regardless of whether or not DB2 or Oracle is used (the customer doesn't really 
like this idea though).  The next fix, which only works on DB2, is to set 
DBDictionary.datePrecision=1 (nanosecond precision).  This still doesn't 
resolve the issue on Oracle.  To see why this is the case, we have to look at 
the way DB2 and Oracle appear to handle timestamp precision.  Both seem to use 
microsecond precision by default (for both I think you have the option to go up 
to nanosecond).  Where they differ is that for precision greater than 
microsecond, DB2 truncates (no rounding) to a microsec.  However, Oracle rounds 
to the microsec.  So, lets take the value 2014-01-22 07:22:11.548778567.  If 
datePrecision=1 we'd send to the DB 2014-01-22 07:22:11.548778567.  On DB2, it 
would truncate the value to 2014-01-22 07:22:11.548778, but on Oracle, it would 
round to: 2014-01-22 07:22:11.548779.  So, for DB2, the compare would be:
in-mem:  2014-01-22 07:22:11.548778567
from db: 2014-01-22 07:22:11.548778

This compare passes, so all is well.  For Oracle, we'd compare:
in-mem:  2014-01-22 07:22:11.548778567
from db: 2014-01-22 07:22:11.548779

This compare fails, thus the OptLockEx.  I think setting datePrecision=1 is a 
fair recommendation/fix for DB2 (others?)......but for Oracle (others?) a fix 
is needed.  My plan is to provide a fix which rounds the in-memory version of 
the timestamp to the value set in DBDictionary.datePrecision.  So the exception 
can still occur if a user has not set datePrecision to match the precision of 
the Timestamp defined in the database.

Thanks,

Heath Thomann



--
This message was sent by Atlassian JIRA
(v6.1.5#6160)

Reply via email to