You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openjpa.apache.org by rp...@apache.org on 2010/04/30 21:07:58 UTC

svn commit: r939783 - in /openjpa/branches/1.1.x: openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/ openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/

Author: rpalache
Date: Fri Apr 30 19:07:58 2010
New Revision: 939783

URL: http://svn.apache.org/viewvc?rev=939783&view=rev
Log:
OpenJPA-466 Applied B.J. Reed's 1.2.x patch to 1.1.x batch.

Added:
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java
    openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java
Modified:
    openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/AbstractJDBCSeq.java
    openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java

Modified: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/AbstractJDBCSeq.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/AbstractJDBCSeq.java?rev=939783&r1=939782&r2=939783&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/AbstractJDBCSeq.java (original)
+++ openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/AbstractJDBCSeq.java Fri Apr 30 19:07:58 2010
@@ -57,8 +57,9 @@ public abstract class AbstractJDBCSeq
     public Object next(StoreContext ctx, ClassMetaData meta) {
         JDBCStore store = getStore(ctx);
         try {
-            current = nextInternal(store, (ClassMapping) meta);
-            return current;
+            Object currentLocal = nextInternal(store, (ClassMapping) meta);
+            current = currentLocal;
+            return currentLocal;
         } catch (OpenJPAException ke) {
             throw ke;
         } catch (SQLException se) {

Modified: openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java?rev=939783&r1=939782&r2=939783&view=diff
==============================================================================
--- openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java (original)
+++ openjpa/branches/1.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/NativeJDBCSeq.java Fri Apr 30 19:07:58 2010
@@ -272,7 +272,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);
 

Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java?rev=939783&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityEmployee.java Fri Apr 30 19:07:58 2010
@@ -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;
+    }
+}
\ No newline at end of file

Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java?rev=939783&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/EntityPerson.java Fri Apr 30 19:07:58 2010
@@ -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;
+    } 
+}
\ No newline at end of file

Added: openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java
URL: http://svn.apache.org/viewvc/openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java?rev=939783&view=auto
==============================================================================
--- openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java (added)
+++ openjpa/branches/1.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/sequence/TestSequence.java Fri Apr 30 19:07:58 2010
@@ -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 <code>iterations</code> times in
+     * <code>threads</code> 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");
+    }
+}
\ No newline at end of file