diff --git a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/JDBCStoreManager.java b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/JDBCStoreManager.java index 5a56d2d71..893da73cf 100644 --- a/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/JDBCStoreManager.java +++ b/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/kernel/JDBCStoreManager.java @@ -305,10 +305,18 @@ public class JDBCStoreManager try { return mapping.getVersion().checkVersion(sm, this, true); } catch (SQLException se) { - throw SQLExceptions.getStore(se, _dict); + throw SQLExceptions.getStore(se, _dict, getReadLockLevel()); } } + private int getReadLockLevel() { + JDBCFetchConfiguration fetch = getFetchConfiguration(); + if (fetch != null) { + return fetch.getReadLockLevel(); + } + return -1; + } + public int compareVersion(OpenJPAStateManager state, Object v1, Object v2) { ClassMapping mapping = (ClassMapping) state.getMetaData(); return mapping.getVersion().compareVersion(v1, v2); diff --git a/openjpa-persistence-locking/src/test/java/org/apache/openjpa/persistence/lockmgr/TestPessimisticLocks.java b/openjpa-persistence-locking/src/test/java/org/apache/openjpa/persistence/lockmgr/TestPessimisticLocks.java index 0f19d0dc7..a46c13971 100644 --- a/openjpa-persistence-locking/src/test/java/org/apache/openjpa/persistence/lockmgr/TestPessimisticLocks.java +++ b/openjpa-persistence-locking/src/test/java/org/apache/openjpa/persistence/lockmgr/TestPessimisticLocks.java @@ -21,6 +21,11 @@ package org.apache.openjpa.persistence.lockmgr; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import javax.persistence.EntityManager; import javax.persistence.LockModeType; @@ -32,7 +37,10 @@ import javax.persistence.TypedQuery; import junit.framework.AssertionFailedError; import org.apache.openjpa.jdbc.conf.JDBCConfiguration; +import org.apache.openjpa.jdbc.sql.DB2Dictionary; import org.apache.openjpa.jdbc.sql.DBDictionary; +import org.apache.openjpa.jdbc.sql.DerbyDictionary; +import org.apache.openjpa.lib.log.Log; import org.apache.openjpa.persistence.LockTimeoutException; import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI; import org.apache.openjpa.persistence.test.SQLListenerTestCase; @@ -65,7 +73,7 @@ public class TestPessimisticLocks extends SQLListenerTestCase { if (isTestsDisabled()) return; - setUp(CLEAR_TABLES, Employee.class, Department.class, "openjpa.LockManager", "mixed"); + setUp(CLEAR_TABLES, Employee.class, Department.class, VersionEntity.class, "openjpa.LockManager", "mixed"); EntityManager em = null; em = emf.createEntityManager(); @@ -399,6 +407,82 @@ public class TestPessimisticLocks extends SQLListenerTestCase { em.getTransaction().commit(); } + protected Log getLog() { + return emf.getConfiguration().getLog("Tests"); + } + + /** + * This variation introduces a row level write lock in a secondary thread, + * issues a refresh in the main thread with a lock timeout, and expects a + * LockTimeoutException. + */ + public void testRefreshLockTimeout() { + + // Only run this test on DB2 and Derby for now. It could cause + // the test to hang on other platforms. + if (!(dict instanceof DerbyDictionary || + dict instanceof DB2Dictionary)) { + return; + } + + EntityManager em = emf.createEntityManager(); + + resetSQL(); + VersionEntity ve = new VersionEntity(); + int veid = new Random().nextInt(); + ve.setId(veid); + ve.setName("Versioned Entity"); + + em.getTransaction().begin(); + em.persist(ve); + em.getTransaction().commit(); + + em.getTransaction().begin(); + // Assert that the department can be found and no lock mode is set + ve = em.find(VersionEntity.class, veid); + assertTrue(em.contains(ve)); + assertTrue(em.getLockMode(ve) == LockModeType.NONE); + em.getTransaction().commit(); + + // Kick of a thread to lock the DB for update + ExecutorService executor = Executors.newFixedThreadPool(1); + Future result = executor.submit(new RefreshWithLock(veid, this)); + try { + // Wait for the thread to lock the row + getLog().trace("Main: waiting"); + synchronized (this) { + // The derby lock timeout is configured for 60 seconds, by default. + wait(70000); + } + getLog().trace("Main: done waiting"); + Map props = new HashMap(); + // This property does not have any effect on Derby for the locking + // condition produced by this test. Instead, Derby uses the + // lock timeout value specified in the config (pom.xml) + props.put("javax.persistence.lock.timeout", 5000); + em.getTransaction().begin(); + getLog().trace("Main: refresh with force increment"); + em.refresh(ve, LockModeType.PESSIMISTIC_FORCE_INCREMENT, props); + getLog().trace("Main: commit"); + em.getTransaction().commit(); + getLog().trace("Main: done commit"); + fail("Expected LockTimeoutException"); + } catch (Throwable t) { + getLog().trace("Main: exception - " + t.getMessage(), t); + assertTrue( t instanceof LockTimeoutException); + } finally { + try { + // Wake the thread and wait for the thread to finish + synchronized(this) { + this.notify(); + } + result.get(); + } catch (Throwable t) { + fail("Caught throwable waiting for thread finish: " + t); + } + } + } + /** * Assert that an exception of proper type has been thrown. Also checks that * that the exception has populated the failed object. @@ -435,4 +519,51 @@ public class TestPessimisticLocks extends SQLListenerTestCase { return null; } + /** + * Separate execution thread used to forcing a lock condition on + * a row in the VersionEntity table. + */ + public class RefreshWithLock implements Callable { + + private int _id; + private Object _monitor; + + public RefreshWithLock(int id, Object monitor) { + _id = id; + _monitor = monitor; + } + + public Boolean call() throws Exception { + try { + EntityManager em = emf.createEntityManager(); + + em.getTransaction().begin(); + // Find with pessimistic force increment. Will lock row for duration of TX. + VersionEntity ve = em.find(VersionEntity.class, _id, LockModeType.PESSIMISTIC_FORCE_INCREMENT); + assertTrue(em.getLockMode(ve) == LockModeType.PESSIMISTIC_FORCE_INCREMENT); + // Wake up the main thread + getLog().trace("Thread: wake up main thread"); + synchronized(_monitor) { + _monitor.notify(); + } + // Wait up to 120 seconds for main thread to complete. The default derby timeout is 60 seconds. + try { + getLog().trace("Thread: waiting up to 120 secs for notify"); + synchronized(_monitor) { + _monitor.wait(120000); + } + getLog().trace("Thread: done waiting"); + } catch (Throwable t) { + getLog().trace("Unexpected thread interrupt",t); + } + + em.getTransaction().commit(); + em.close(); + getLog().trace("Thread: done"); + } catch (Throwable t) { + getLog().trace("Thread: caught - " + t.getMessage(), t); + } + return Boolean.TRUE; + } + } } diff --git a/openjpa-persistence-locking/src/test/java/org/apache/openjpa/persistence/lockmgr/VersionEntity.java b/openjpa-persistence-locking/src/test/java/org/apache/openjpa/persistence/lockmgr/VersionEntity.java new file mode 100644 index 000000000..472a83a1c --- /dev/null +++ b/openjpa-persistence-locking/src/test/java/org/apache/openjpa/persistence/lockmgr/VersionEntity.java @@ -0,0 +1,61 @@ +/* + * 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.lockmgr; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Version; + +@Entity +@Table(name="LK_VERSENT") +public class VersionEntity { + + @Id + private int id; + + private String name; + + @Version + private int version; + + public void setId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setVersion(int version) { + this.version = version; + } + + public int getVersion() { + return version; + } +}