From 3f33d76a34d24dee9cc16e68c1d12441ca5631c8 Mon Sep 17 00:00:00 2001 From: Heath Thomann Date: Thu, 1 May 2014 03:24:11 +0000 Subject: [PATCH] OPENJPA-2476: Fixed OptimisticLockEx due to rounding of a Timestamp git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@1591541 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/openjpa/jdbc/sql/DBDictionary.java | 20 +-- .../openjpa/kernel/StateManagerImpl.java | 42 +++++- .../timestamp/TestTimestampOptLockEx.java | 129 ++++++++++++++++++ .../optlockex/timestamp/VersionTSEntity.java | 63 +++++++++ .../src/doc/manual/ref_guide_dbsetup.xml | 33 ++++- 5 files changed, 267 insertions(+), 20 deletions(-) create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java index 94e69c9f2..af7ac1165 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java @@ -96,6 +96,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; @@ -1288,24 +1289,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); } /** diff --git a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java index e8f03dd1a..b4fc048b4 100644 --- a/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java +++ b/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java @@ -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; @@ -159,6 +160,8 @@ public class StateManagerImpl implements OpenJPAStateManager, Serializable { private transient ReentrantLock _instanceLock = null; + private int _datePrecision = -1; + /** *

set to false to prevent the postLoad method from * sending lifecycle callback events.

@@ -716,13 +719,48 @@ public class StateManagerImpl implements OpenJPAStateManager, Serializable { 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() { diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java new file mode 100644 index 000000000..205eb7551 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java @@ -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(); + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java new file mode 100644 index 000000000..8b2945b1e --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java @@ -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; + } +} diff --git a/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml b/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml index 88fc1452a..c6263b809 100644 --- a/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml +++ b/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml @@ -1405,9 +1405,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. - +before storing them in the database. This property can be set to one +of the following precisions: + + + + +DECI: 100000000 + + + + +CENIT: 10000000 + + + + +MILLI (default precision): 1000000 + + + + +MICRO: 1000 + + + + +NANO (max precision): 1 + + +