OPENJPA-2476: Fixed OptimisticLockEx due to rounding of a Timestamp

git-svn-id: https://svn.apache.org/repos/asf/openjpa/branches/2.1.x@1591536 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Heath Thomann 2014-05-01 02:34:15 +00:00
parent 7dec044a1d
commit 9221876ed7
5 changed files with 267 additions and 22 deletions

View File

@ -28,7 +28,6 @@ import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Array;
@ -96,6 +95,7 @@ import org.apache.openjpa.jdbc.schema.ForeignKey.FKMapKey;
import org.apache.openjpa.kernel.Filters;
import org.apache.openjpa.kernel.OpenJPAStateManager;
import org.apache.openjpa.kernel.Seq;
import org.apache.openjpa.kernel.StateManagerImpl;
import org.apache.openjpa.kernel.exps.Path;
import org.apache.openjpa.lib.conf.Configurable;
import org.apache.openjpa.lib.conf.Configuration;
@ -1248,24 +1248,13 @@ public class DBDictionary
public void setTimestamp(PreparedStatement stmnt, int idx,
Timestamp val, Calendar cal, Column col)
throws SQLException {
// ensure that we do not insert dates at a greater precision than
// that at which they will be returned by a SELECT
int rounded = (int) Math.round(val.getNanos() /
(double) datePrecision);
int nanos = rounded * datePrecision;
if (nanos > 999999999) {
// rollover to next second
val.setTime(val.getTime() + 1000);
nanos = 0;
}
Timestamp valForStmnt = new Timestamp(val.getTime());
valForStmnt.setNanos(nanos);
val = StateManagerImpl.roundTimestamp(val, datePrecision);
if (cal == null)
stmnt.setTimestamp(idx, valForStmnt);
stmnt.setTimestamp(idx, val);
else
stmnt.setTimestamp(idx, valForStmnt, cal);
stmnt.setTimestamp(idx, val, cal);
}
/**

View File

@ -25,6 +25,7 @@ import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Modifier;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
@ -34,7 +35,6 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.locks.ReentrantLock;
@ -157,6 +157,8 @@ public class StateManagerImpl
private transient ReentrantLock _instanceLock = null;
private int _datePrecision = -1;
/**
* Constructor; supply id, type metadata, and owning persistence manager.
*/
@ -729,13 +731,48 @@ public class StateManagerImpl
public void setNextVersion(Object version) {
assignVersionField(version);
}
public static Timestamp roundTimestamp(Timestamp val, int datePrecision) {
// ensure that we do not insert dates at a greater precision than
// that at which they will be returned by a SELECT
int rounded = (int) Math.round(val.getNanos() / (double) datePrecision);
long time = val.getTime();
int nanos = rounded * datePrecision;
if (nanos > 999999999) {
// rollover to next second
time = time + 1000;
nanos = 0;
}
val = new Timestamp(time);
val.setNanos(nanos);
return val;
}
private void assignVersionField(Object version) {
if (version instanceof Timestamp) {
if (_datePrecision == -1) {
try {
OpenJPAConfiguration conf = _broker.getConfiguration();
Class confCls = Class.forName("org.apache.openjpa.jdbc.conf.JDBCConfigurationImpl");
if (confCls.isAssignableFrom(conf.getClass())) {
Object o = conf.getClass().getMethod("getDBDictionaryInstance").invoke(conf, (Object[]) null);
_datePrecision = o.getClass().getField("datePrecision").getInt(o);
} else {
_datePrecision = 1000;
}
} catch (Throwable e) {
_datePrecision = 1000;
}
}
version = roundTimestamp((Timestamp) version, _datePrecision);
}
_version = version;
FieldMetaData vfield = _meta.getVersionField();
if (vfield != null)
store(vfield.getIndex(), JavaTypes.convert(version,
vfield.getTypeCode()));
store(vfield.getIndex(), JavaTypes.convert(version, vfield.getTypeCode()));
}
public PCState getPCState() {

View File

@ -0,0 +1,129 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.persistence.optlockex.timestamp;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import org.apache.openjpa.persistence.test.SingleEMFTestCase;
/*
* Test create for JIRA OPENJPA-2476, see it for a very detailed
* description of the issue.
*/
public class TestTimestampOptLockEx extends SingleEMFTestCase {
@Override
public void setUp() {
// By default we'd round a Timestamp to the nearest millisecond on Oracle (see DBDictionary.datePrecision
// and DBDictionary.setTimestamp) and nearest microsecond on DB2 (see DB2Dictionary.datePrecision and
// DBDictionary.setTimestamp) when sending the value to the db...if we change datePrecision to 1, we round to
// the nearest nanosecond. On DB2 and Oracle, it appears the default precision is microseconds but it seems
// DB2 truncates (no rounding) to microsecond for anything it is given with greater precision, whereas Oracle
// rounds. So in the case of DB2, this test will pass if datePrecision=1, but still fails on Oracle.
// On the other hand, if we set the datePrecision to 1000000 and run against DB2, the test will fail.
// This test requires datePrecision to be set to the same precision as the Timestamp column.
// I've only been testing on Oracle and DB2 and not sure how other DBs treat a Timestamps precision
// by default. In VersionTSEntity I use a Timestamp(3) but this is not supported on, at least, Derby
// and older versions of DB2...at this time I'll enable only on Oracle.
setSupportedDatabases(org.apache.openjpa.jdbc.sql.OracleDictionary.class);
if (isTestsDisabled()) {
return;
}
// Set datePrecision=1000000 for Oracle since we are using Timestamp(3)....on Oracle
// the default is 1000000 so we shouldn't need to set it, but lets set it to future
// proof the test.
super.setUp(DROP_TABLES, "openjpa.jdbc.DBDictionary", "datePrecision=1000000", VersionTSEntity.class);
}
public void testUpdate() {
poplulate();
//This loop is necessary since we need a timestamp which has been rounded up
//by the database, or by OpenJPA such that the in-memory version of the Timestamp
//varies from that which is in the database.
for (int i = 0; i < 50000; i++) {
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// Find an existing VersionTSEntity:
// stored with microsecond precision, e.g. 2014-01-21 13:16:46.595428
VersionTSEntity t = em.find(VersionTSEntity.class, 1);
tx.begin();
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.
// now, do a merge with the unchanged entity
tx = em.getTransaction();
tx.begin();
t = em.merge(t); // this results in a select of VersionTSEntity
//This 'fixes' the issue (but customer doesn't really want to add this):
//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 ColumnVersionStrategy.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();
em.close();
}
}
public void poplulate(){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
VersionTSEntity r = new VersionTSEntity();
r.setId(1L);
r.setSomeInt(0);
em.persist(r);
tx.commit();
em.close();
}
}

View File

@ -0,0 +1,63 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.openjpa.persistence.optlockex.timestamp;
import java.io.Serializable;
import java.sql.Timestamp;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
@Entity
public class VersionTSEntity implements Serializable {
private static final long serialVersionUID = 2948711625184868242L;
@Id
private Long id;
@Version
@Column(columnDefinition="TIMESTAMP(3)")
private Timestamp updateTimestamp;
private Integer someInt;
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public Timestamp getUpdateTimestamp() {
return this.updateTimestamp;
}
public void setSomeInt(Integer someInt) {
this.someInt = someInt;
}
public Integer getSomeInt() {
return someInt;
}
}

View File

@ -1377,9 +1377,36 @@ This value is usually one million, meaning that the database is able
to store time values with a precision of one millisecond. Particular
databases may have more or less precision.
OpenJPA will round all time values to this degree of precision
before storing them in the database.
Defaults to 1000000.
</para>
before storing them in the database. This property can be set to one
of the following precisions:
</para>
<itemizedlist>
<listitem>
<para>
<literal>DECI</literal>: 100000000
</para>
</listitem>
<listitem>
<para>
<literal>CENIT</literal>: 10000000
</para>
</listitem>
<listitem>
<para>
<literal>MILLI (default precision)</literal>: 1000000
</para>
</listitem>
<listitem>
<para>
<literal>MICRO</literal>: 1000
</para>
</listitem>
<listitem>
<para>
<literal>NANO (max precision)</literal>: 1
</para>
</listitem>
</itemizedlist>
</listitem>
<listitem id="DBDictionary.DateMillisecondBehavior">
<para>