From 679989fd16a96bd9f94c0a68face0374dfaa2b44 Mon Sep 17 00:00:00 2001 From: Albert Lee Date: Thu, 19 Feb 2009 05:34:30 +0000 Subject: [PATCH] OPENJPA-466 - Commit patch contributed by Tim McConnell. git-svn-id: https://svn.apache.org/repos/asf/openjpa/trunk@745737 13f79535-47bb-0310-9956-ffa450edef68 --- .../openjpa/jdbc/kernel/NativeJDBCSeq.java | 4 +- .../persistence/sequence/EntityEmployee.java | 138 +++++ .../persistence/sequence/EntityPerson.java | 132 +++++ .../persistence/sequence/TestSequence.java | 543 ++++++++++++++++++ 4 files changed, 816 insertions(+), 1 deletion(-) create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java create mode 100644 openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java index b49372693..348e2d621 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java @@ -294,7 +294,9 @@ public class NativeJDBCSeq ResultSet rs = null; try { stmnt = conn.prepareStatement(_select); - rs = stmnt.executeQuery(); + synchronized(this) { + rs = stmnt.executeQuery(); + } if (rs.next()) return rs.getLong(1); diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java new file mode 100644 index 000000000..8980e6611 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java @@ -0,0 +1,138 @@ +/* + * 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.sequence; + +import java.io.Serializable; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +/** + * @author Tim McConnell + * @since 2.0.0 + */ +@Entity +@Table(name="ENTITY_EMPLOYEE") +public class EntityEmployee implements Serializable { + + private static final long serialVersionUID = 2961572787273807912L; + + @Id + @SequenceGenerator(name="SeqEmployee", sequenceName="test_native_sequence") + @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SeqEmployee") + private int id; + private String firstName; + private String lastName; + private float salary; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public float getSalary() { + return salary; + } + + public void setSalary(float salary) { + this.salary = salary; + } + + @Override + public String toString() { + return "EntityEmployee: Employee id: " + getId() + + " firstName: " + getFirstName() + + " lastName: " + getLastName() + + " salary: " + getSalary(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((getFirstName() == null) ? 0 : getFirstName().hashCode()); + result = prime * result + getId(); + result = prime * result + + ((getLastName() == null) ? 0 : getLastName().hashCode()); + result = prime * result + Float.floatToIntBits(getSalary()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final EntityEmployee other = (EntityEmployee) obj; + if (getId() != other.getId()) { + return false; + } + if (getFirstName() == null) { + if (other.getFirstName() != null) { + return false; + } + } + else if (!getFirstName().equals(other.getFirstName())) { + return false; + } + if (getLastName() == null) { + if (other.getLastName() != null) { + return false; + } + } + else if (!getLastName().equals(other.getLastName())) { + return false; + } + if (Float.floatToIntBits(getSalary()) != Float.floatToIntBits(other + .getSalary())) { + return false; + } + return true; + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java new file mode 100644 index 000000000..e8c925c4a --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java @@ -0,0 +1,132 @@ +/* + * 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.sequence; + +import java.io.Serializable; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +/** + * @author Tim McConnell + * @since 2.0.0 + */ +@Entity +@Table(name="ENTITY_PERSON") +public class EntityPerson implements Serializable { + + private static final long serialVersionUID = 3772049669261731520L; + + @Id + @SequenceGenerator(name="SeqPerson", sequenceName="test_native_sequence") + @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SeqPerson") + private int id; + private String firstName; + private String lastName; + + + public EntityPerson() { + } + + public EntityPerson(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public String toString() { + return "EntityPerson: Person id: " + getId() + + " firstName: " + getFirstName() + + " lastName: " + getLastName(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((getFirstName() == null) ? 0 : getFirstName().hashCode()); + result = prime * result + getId(); + result = prime * result + + ((getLastName() == null) ? 0 : getLastName().hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final EntityPerson other = (EntityPerson) obj; + if (getId() != other.getId()) { + return false; + } + if (getFirstName() == null) { + if (other.getFirstName() != null) { + return false; + } + } + else if (!getFirstName().equals(other.getFirstName())) { + return false; + } + if (getLastName() == null) { + if (other.getLastName() != null) { + return false; + } + } + else if (!getLastName().equals(other.getLastName())) { + return false; + } + return true; + } +} diff --git a/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java new file mode 100644 index 000000000..ee9075ad3 --- /dev/null +++ b/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java @@ -0,0 +1,543 @@ +/* + * 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 agEmployee_Last_Name 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.sequence; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; + +import javax.persistence.EntityManager; + +import org.apache.openjpa.jdbc.conf.JDBCConfiguration; +import org.apache.openjpa.persistence.test.SingleEMFTestCase; + +/** + * @author Tim McConnell + * @since 2.0.0 + */ +public class TestSequence extends SingleEMFTestCase { + + private String multiThreadExecuting = null; + private static final int NUMBER_ENTITIES = 5000; + + public void setUp() { + setUp(EntityPerson.class, EntityEmployee.class, CLEAR_TABLES, + "openjpa.Multithreaded", "true"); + } + + // Override teardown to preserve database contents + @Override + public void tearDown() throws Exception { + } + + public void testMultiThreadedNativeSequences() throws Exception { + boolean supportsNativeSequence = false; + + try { + supportsNativeSequence = ((JDBCConfiguration) emf + .getConfiguration()).getDBDictionaryInstance() + .nextSequenceQuery != null; + } catch (Throwable t) { + supportsNativeSequence = false; + } + + if (supportsNativeSequence) { + mttest(6, 8); + switch ((int) (Math.random() * 7)) { + case 0: + createAndRemove(); + break; + case 1: + createManyPersonsInSeparateTransactions(); + break; + case 2: + createManyEmployeesInSeparateTransactions(); + break; + case 3: + createManyPersonsAndEmployeesInSeparateTransactions(); + break; + case 4: + createManyPersonsInSingleTransaction(); + break; + case 5: + createManyEmployeesInSingleTransaction(); + break; + case 6: + createManyPersonsAndEmployeesInSingleTransaction(); + break; + } + } + } + + private void createAndRemove() { + int person_id; + int employee_id; + + EntityManager em = emf.createEntityManager(); + + EntityPerson person = new EntityPerson(); + person.setFirstName("Person_First_Name"); + person.setLastName("Person_Last_Name"); + + EntityEmployee employee = new EntityEmployee(); + employee.setFirstName("Employee_First_Name"); + employee.setLastName("Employee_Last_Name"); + employee.setSalary(NUMBER_ENTITIES); + + em.getTransaction().begin(); + em.persist(person); + em.persist(employee); + em.getTransaction().commit(); + + em.refresh(person); + em.refresh(employee); + person_id = person.getId(); + employee_id = employee.getId(); + + person = em.find(EntityPerson.class, person_id); + assertTrue(person != null); + assertTrue(person.getId() == person_id); + assertTrue(person.getFirstName().equals("Person_First_Name")); + assertTrue(person.getLastName().equals("Person_Last_Name")); + + employee = em.find(EntityEmployee.class, employee_id); + assertTrue(employee != null); + assertTrue(employee.getId() == employee_id); + assertTrue(employee.getFirstName().equals("Employee_First_Name")); + assertTrue(employee.getLastName().equals("Employee_Last_Name")); + assertTrue(employee.getSalary() == NUMBER_ENTITIES); + + em.getTransaction().begin(); + em.remove(person); + em.remove(employee); + em.getTransaction().commit(); + + em.clear(); + em.close(); + } + + private void createManyPersonsInSeparateTransactions() { + EntityManager em = emf.createEntityManager(); + + for (int ii = 0; ii < NUMBER_ENTITIES; ii++) { + EntityPerson person = new EntityPerson(); + person.setFirstName("1_First_name_" + ii); + person.setLastName("1_Last_name_" + ii); + + em.getTransaction().begin(); + em.persist(person); + em.getTransaction().commit(); + } + + em.clear(); + em.close(); + } + + private void createManyEmployeesInSeparateTransactions() { + EntityManager em = emf.createEntityManager(); + + for (int ii = 0; ii < NUMBER_ENTITIES; ii++) { + EntityEmployee employee = new EntityEmployee(); + employee.setFirstName("2_First_name_" + ii); + employee.setLastName("2_Last_name_" + ii); + employee.setSalary(ii); + + em.getTransaction().begin(); + em.persist(employee); + em.getTransaction().commit(); + } + + em.clear(); + em.close(); + } + + private void createManyPersonsAndEmployeesInSeparateTransactions() { + EntityManager em = emf.createEntityManager(); + + for (int ii = 0; ii < NUMBER_ENTITIES; ii++) { + EntityPerson person = new EntityPerson(); + person.setFirstName("3_First_name_" + ii); + person.setLastName("3_Last_name_" + ii); + + EntityEmployee employee = new EntityEmployee(); + employee.setFirstName("4_First_name_" + ii); + employee.setLastName("4_Last_name_" + ii); + employee.setSalary(ii); + + em.getTransaction().begin(); + em.persist(person); + em.persist(employee); + em.getTransaction().commit(); + } + + em.clear(); + em.close(); + } + + private void createManyPersonsInSingleTransaction() { + EntityManager em = emf.createEntityManager(); + + em.getTransaction().begin(); + for (int ii = 0; ii < NUMBER_ENTITIES; ii++) { + EntityPerson person = new EntityPerson(); + person.setFirstName("5_First_name_" + ii); + person.setLastName("5_Last_name_" + ii); + + em.persist(person); + } + em.getTransaction().commit(); + + em.clear(); + em.close(); + } + + private void createManyEmployeesInSingleTransaction() { + EntityManager em = emf.createEntityManager(); + + em.getTransaction().begin(); + for (int ii = 0; ii < NUMBER_ENTITIES; ii++) { + EntityEmployee employee = new EntityEmployee(); + employee.setFirstName("6_First_name_" + ii); + employee.setLastName("6_Last_name_" + ii); + employee.setSalary(ii); + + em.persist(employee); + } + em.getTransaction().commit(); + + em.clear(); + em.close(); + } + + private void createManyPersonsAndEmployeesInSingleTransaction() { + EntityManager em = emf.createEntityManager(); + + em.getTransaction().begin(); + for (int ii = 0; ii < NUMBER_ENTITIES; ii++) { + EntityPerson person = new EntityPerson(); + person.setFirstName("7_First_name_" + ii); + person.setLastName("7_Last_name_" + ii); + + EntityEmployee employee = new EntityEmployee(); + employee.setFirstName("8_First_name_" + ii); + employee.setLastName("8_Last_name_" + ii); + employee.setSalary(ii); + + em.persist(person); + em.persist(employee); + } + em.getTransaction().commit(); + + em.clear(); + em.close(); + } + + /** + * Re-execute the invoking method a random number of times in a random + * number of Threads. + */ + public void mttest() throws ThreadingException { + // 6 iterations in 8 threads is a good trade-off between + // tests taking way too long and having a decent chance of + // identifying MT problems. + int iterations = 6; + int threads = 8; + + mttest(threads, iterations); + } + + /** + * Execute the calling method iterations times in + * threads Threads. + */ + public void mttest(int threads, int iterations) { + mttest(0, threads, iterations); + } + + public void mttest(int serialCount, int threads, int iterations) + throws ThreadingException { + String methodName = callingMethod("mttest"); + mttest(serialCount, threads, iterations, methodName, new Object[0]); + } + + /** + * Execute a test method in multiple threads. + * + * @param threads + * the number of Threads to run in + * @param iterations + * the number of times the method should be execute in a single + * Thread + * @param method + * the name of the method to execute + * @param args + * the arguments to pass to the method + * @throws ThreadingException + * if an errors occur in any of the Threads. The actual + * exceptions will be embedded in the exception. Note that this + * means that assert() failures will be treated as errors rather + * than warnings. + * @author Marc Prud'hommeaux + */ + public void mttest(int threads, int iterations, final String method, + final Object[] args) throws ThreadingException { + mttest(0, threads, iterations, method, args); + } + + public void mttest(int serialCount, int threads, int iterations, + final String method, final Object[] args) throws ThreadingException { + if (multiThreadExecuting != null + && multiThreadExecuting.equals(method)) { + // we are currently executing in multi-threaded mode: + // don't deadlock! + return; + } + + multiThreadExecuting = method; + + try { + Class[] paramClasses = new Class[args.length]; + for (int i = 0; i < paramClasses.length; i++) + paramClasses[i] = args[i].getClass(); + + final Method meth; + + try { + meth = getClass().getMethod(method, paramClasses); + } catch (NoSuchMethodException nsme) { + throw new ThreadingException(nsme.toString(), nsme); + } + + final Object thiz = this; + + mttest("reflection invocation: (" + method + ")", serialCount, + threads, iterations, new VolatileRunnable() { + public void run() throws Exception { + meth.invoke(thiz, args); + } + }); + } finally { + multiThreadExecuting = null; + } + } + + public void mttest(String title, final int threads, final int iterations, + final VolatileRunnable runner) throws ThreadingException { + mttest(title, 0, threads, iterations, runner); + } + + /** + * Execute a test method in multiple threads. + * + * @param title + * a description of the test, for inclusion in the error message + * @param serialCount + * the number of times to run the method serially before spawning + * threads. + * @param threads + * the number of Threads to run in + * @param iterations + * the number of times the method should + * @param runner + * the VolatileRunnable that will execute the actual test from + * within the Thread. + * @throws ThreadingException + * if an errors occur in any of the Threads. The actual + * exceptions will be embedded in the exception. Note that this + * means that assert() failures will be treated as errors rather + * than warnings. + * @author Marc Prud'hommeaux + */ + public void mttest(String title, final int serialCount, final int threads, + final int iterations, final VolatileRunnable runner) + throws ThreadingException { + final List exceptions = Collections.synchronizedList(new LinkedList()); + + Thread[] runners = new Thread[threads]; + + final long startMillis = System.currentTimeMillis() + 1000; + + for (int i = 1; i <= threads; i++) { + final int thisThread = i; + + runners[i - 1] = new Thread(title + " [" + i + " of " + threads + + "]") { + public void run() { + // do our best to have all threads start at the exact + // same time. This is imperfect, but the closer we + // get to everyone starting at the same time, the + // better chance we have for identifying MT problems. + while (System.currentTimeMillis() < startMillis) + yield(); + + int thisIteration = 1; + try { + for (; thisIteration <= iterations; thisIteration++) { + // go go go! + runner.run(); + } + } catch (Throwable error) { + synchronized (exceptions) { + // embed the exception into something that gives + // us some more information about the threading + // environment + exceptions.add(new ThreadingException("thread=" + + this.toString() + ";threadNum=" + thisThread + + ";maxThreads=" + threads + ";iteration=" + + thisIteration + ";maxIterations=" + + iterations, error)); + } + } + } + }; + } + + // start the serial tests(does not spawn the threads) + for (int i = 0; i < serialCount; i++) { + runners[0].run(); + } + + // start the multithreaded + for (int i = 0; i < threads; i++) { + runners[i].start(); + } + + // wait for them all to complete + for (int i = 0; i < threads; i++) { + try { + runners[i].join(); + } catch (InterruptedException e) { + } + } + + if (exceptions.size() == 0) + return; // sweeeeeeeet: no errors + + // embed all the exceptions that were throws into a + // ThreadingException + Throwable[] errors = (Throwable[]) exceptions.toArray(new Throwable[0]); + throw new ThreadingException("The " + errors.length + + " embedded errors " + "occured in the execution of " + iterations + + " iterations " + "of " + threads + " threads: [" + title + "]", + errors); + } + + /** + * Check to see if we are in the top-level execution stack. + */ + public boolean isRootThread() { + return multiThreadExecuting == null; + } + + /** + * A Runnable that can throw an Exception: used to test cases. + */ + public static interface VolatileRunnable { + + public void run() throws Exception; + } + + /** + * Exception for errors caught during threading tests. + */ + public class ThreadingException extends RuntimeException { + + private static final long serialVersionUID = -1911769845552507956L; + private final Throwable[] _nested; + + public ThreadingException(String msg, Throwable nested) { + super(msg); + if (nested == null) + _nested = new Throwable[0]; + else + _nested = new Throwable[] { nested }; + } + + public ThreadingException(String msg, Throwable[] nested) { + super(msg); + if (nested == null) + _nested = new Throwable[0]; + else + _nested = nested; + } + + public void printStackTrace() { + printStackTrace(System.out); + } + + public void printStackTrace(PrintStream out) { + printStackTrace(new PrintWriter(out)); + } + + public void printStackTrace(PrintWriter out) { + super.printStackTrace(out); + for (int i = 0; i < _nested.length; i++) { + out.print("Nested Throwable #" + (i + 1) + ": "); + _nested[i].printStackTrace(out); + } + } + } + + /** + * Return the last method name that called this one by parsing the current + * stack trace. + * + * @param exclude + * a method name to skip + * @throws IllegalStateException + * If the calling method could not be identified. + * @author Marc Prud'hommeaux + */ + public String callingMethod(String exclude) { + // determine the currently executing method by + // looking at the stack track. Hackish, but convenient. + StringWriter sw = new StringWriter(); + new Exception().printStackTrace(new PrintWriter(sw)); + for (StringTokenizer stackTrace = new StringTokenizer(sw.toString(), + System.getProperty("line.separator")) + ; stackTrace.hasMoreTokens() ; ) { + String line = stackTrace.nextToken().trim(); + + // not a stack trace element + if (!(line.startsWith("at "))) + continue; + + String fullMethodName = line.substring(0, line.indexOf("(")); + + String shortMethodName = fullMethodName.substring(fullMethodName + .lastIndexOf(".") + 1); + + // skip our own methods! + if (shortMethodName.equals("callingMethod")) + continue; + if (exclude != null && shortMethodName.equals(exclude)) + continue; + + return shortMethodName; + } + + throw new IllegalStateException("Could not identify calling " + + "method in stack trace"); + } +}