blob: e74d213f3c7d49d853ddfec059c937bd10d4ba5b [file] [log] [blame]
/*
* 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 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;
import javax.persistence.PessimisticLockException;
import javax.persistence.Query;
import javax.persistence.QueryTimeoutException;
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.jdbc.sql.InformixDictionary;
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;
import org.apache.openjpa.util.OpenJPAException;
/**
* Test Pessimistic Lock and exception behavior against EntityManager and Query
* interface methods.
*/
public class TestPessimisticLocks extends SQLListenerTestCase {
private DBDictionary dict = null;
private int lockWaitTime = 2000;
@Override
protected String getPersistenceUnitName() {
return "locking-test";
}
public void setUp() {
// Disable tests for any DB that has supportsQueryTimeout==false, like Postgres
OpenJPAEntityManagerFactorySPI tempEMF = emf;
if (tempEMF == null) {
tempEMF = createEMF();
}
assertNotNull(tempEMF);
dict = ((JDBCConfiguration)tempEMF.getConfiguration()).getDBDictionaryInstance();
assertNotNull(dict);
if (!dict.supportsQueryTimeout)
setTestsDisabled(true);
if (emf == null) {
closeEMF(tempEMF);
}
if (isTestsDisabled())
return;
setUp(CLEAR_TABLES, Employee.class, Department.class, VersionEntity.class, "openjpa.LockManager", "mixed");
EntityManager em = null;
em = emf.createEntityManager();
em.getTransaction().begin();
Employee e1, e2;
Department d1, d2;
d1 = new Department();
d1.setId(10);
d1.setName("D10");
e1 = new Employee();
e1.setId(1);
e1.setDepartment(d1);
e1.setFirstName("first.1");
e1.setLastName("last.1");
d2 = new Department();
d2.setId(20);
d2.setName("D20");
e2 = new Employee();
e2.setId(2);
e2.setDepartment(d2);
e2.setFirstName("first.2");
e2.setLastName("last.2");
em.persist(d1);
em.persist(d2);
em.persist(e1);
em.persist(e2);
em.getTransaction().commit();
em.close();
}
/*
* Test find with pessimistic lock after a query with pessimistic lock.
*/
public void testFindAfterQueryWithPessimisticLocks() {
EntityManager em1 = emf.createEntityManager();
EntityManager em2 = emf.createEntityManager();
em1.getTransaction().begin();
TypedQuery<Employee> query = em1.createQuery("select e from Employee e where e.id < 10", Employee.class)
.setFirstResult(1);
// Lock all selected Employees, skip the first one, i.e should lock
// Employee(2)
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
List<Employee> employees = query.getResultList();
assertEquals("Expected 1 element with emplyee id=2", employees.size(), 1);
assertTrue("Test Employee first name = 'first.2'", employees.get(0).getFirstName().equals("first.1")
|| employees.get(0).getFirstName().equals("first.2"));
em2.getTransaction().begin();
Map<String, Object> hints = new HashMap<String, Object>();
hints.put("javax.persistence.lock.timeout", lockWaitTime);
// find Employee(2) with a lock, should block and expected a
// PessimisticLockException
try {
em2.find(Employee.class, 2, LockModeType.PESSIMISTIC_READ, hints);
fail("Unexcpected find succeeded. Should throw a PessimisticLockException.");
} catch (Throwable e) {
assertError(e, PessimisticLockException.class, LockTimeoutException.class);
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em1.getTransaction().begin();
TypedQuery<Department> query2 = em1.createQuery("select e.department from Employee e where e.id < 10",
Department.class).setFirstResult(1);
// Lock all selected Departments, skip the first one, i.e should
// lock Department(20)
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
List<Department> depts = query2.getResultList();
assertEquals("Expected 1 element with department id=20", depts.size(), 1);
assertTrue("Test department name = 'D20'", depts.get(0).getName().equals("D10")
|| depts.get(0).getName().equals("D20"));
em2.getTransaction().begin();
Map<String, Object> map = new HashMap<String, Object>();
map.put("javax.persistence.lock.timeout", lockWaitTime);
// find Employee(2) with a lock, no block since only department was
// locked
try {
Employee emp = em2.find(Employee.class, 1, LockModeType.PESSIMISTIC_READ, map);
assertNotNull("Query locks department only, therefore should find Employee.", emp);
assertEquals("Test Employee first name = 'first.1'", emp.getFirstName(), "first.1");
} catch (Exception ex) {
fail("Caught unexpected " + ex.getClass().getName() + ":" + ex.getMessage());
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em1.close();
em2.close();
}
/*
* Test find with pessimistic lock after a query with pessimistic lock.
*/
public void testFindAfterQueryOrderByWithPessimisticLocks() {
EntityManager em1 = emf.createEntityManager();
EntityManager em2 = emf.createEntityManager();
em1.getTransaction().begin();
Query query = em1.createQuery("select e from Employee e where e.id < 10 order by e.id").setFirstResult(1);
// Lock all selected Employees, skip the first one, i.e should lock
// Employee(2)
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
List<Employee> q = query.getResultList();
assertEquals("Expected 1 element with emplyee id=2", q.size(), 1);
assertEquals("Test Employee first name = 'first.2'", q.get(0).getFirstName(), "first.2");
em2.getTransaction().begin();
Map<String, Object> map = new HashMap<String, Object>();
map.put("javax.persistence.lock.timeout", lockWaitTime);
// find Employee(2) with a lock, should block and expected a
// PessimisticLockException
try {
em2.find(Employee.class, 2, LockModeType.PESSIMISTIC_READ, map);
fail("Unexcpected find succeeded. Should throw a PessimisticLockException.");
} catch (Exception e) {
assertError(e, PessimisticLockException.class, LockTimeoutException.class);
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em1.getTransaction().begin();
query = em1.createQuery("select e.department from Employee e where e.id < 10 order by e.department.id")
.setFirstResult(1);
// Lock all selected Departments, skip the first one, i.e should
// lock Department(20)
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
List<Department> result = query.getResultList();
assertEquals("Expected 1 element with department id=20", q.size(), 1);
assertEquals("Test department name = 'D20'", result.get(0).getName(), "D20");
em2.getTransaction().begin();
map.clear();
map.put("javax.persistence.lock.timeout", lockWaitTime);
// find Employee(2) with a lock, no block since only department was
// locked
try {
Employee emp = em2.find(Employee.class, 1, LockModeType.PESSIMISTIC_READ, map);
assertNotNull("Query locks department only, therefore should find Employee.", emp);
assertEquals("Test Employee first name = 'first.1'", emp.getFirstName(), "first.1");
} catch (Exception ex) {
if (!dict.supportsLockingWithOrderClause)
fail("Caught unexpected " + ex.getClass().getName() + ":" + ex.getMessage());
else
assertError(ex, LockTimeoutException.class);
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em1.close();
em2.close();
}
/*
* Test query with pessimistic lock after a find with pessimistic lock.
*/
public void testQueryAfterFindWithPessimisticLocks() {
EntityManager em1 = emf.createEntityManager();
EntityManager em2 = emf.createEntityManager();
try {
em2.getTransaction().begin();
Map<String, Object> map = new HashMap<String, Object>();
map.put("javax.persistence.lock.timeout", lockWaitTime);
// Lock Emplyee(1), no department should be locked
em2.find(Employee.class, 1, LockModeType.PESSIMISTIC_READ, map);
em1.getTransaction().begin();
Query query = em1.createQuery("select e.department from Employee e where e.id < 10").setFirstResult(1);
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
// Lock all selected Department but skip the first, i.e. lock
// Department(20), should query successfully.
List<Department> q = query.getResultList();
assertEquals("Expected 1 element with department id=20", q.size(), 1);
assertTrue("Test department name = 'D20'", q.get(0).getName().equals("D10")
|| q.get(0).getName().equals("D20"));
} catch (Exception ex) {
assertError(ex, QueryTimeoutException.class);
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em2.getTransaction().begin();
Map<String, Object> map = new HashMap<String, Object>();
map.put("javax.persistence.lock.timeout", lockWaitTime);
// Lock Emplyee(2), no department should be locked
em2.find(Employee.class, 2, LockModeType.PESSIMISTIC_READ, map);
em1.getTransaction().begin();
Query query = em1.createQuery("select e from Employee e where e.id < 10").setFirstResult(1);
// Lock all selected Employees, skip the first one, i.e should lock
// Employee(2)
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
try {
List<Employee> q = query.getResultList();
fail("Unexcpected find succeeded. Should throw a PessimisticLockException.");
} catch (Exception e) {
assertError(e, PessimisticLockException.class, QueryTimeoutException.class);
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em1.close();
em2.close();
}
/*
* Test query with pessimistic lock after a find with pessimistic lock.
*/
public void testQueryOrderByAfterFindWithPessimisticLocks() {
EntityManager em1 = emf.createEntityManager();
EntityManager em2 = emf.createEntityManager();
em2.getTransaction().begin();
Map<String, Object> map = new HashMap<String, Object>();
map.put("javax.persistence.lock.timeout", lockWaitTime);
// Lock Emplyee(1), no department should be locked
em2.find(Employee.class, 1, LockModeType.PESSIMISTIC_READ, map);
em1.getTransaction().begin();
Query query = em1.createQuery("select e.department from Employee e where e.id < 10 order by e.department.id")
.setFirstResult(1);
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
// Lock all selected Department but skip the first, i.e. lock
// Department(20), should query successfully.
try {
List<Department> q = query.getResultList();
assertEquals("Expected 1 element with department id=20", q.size(), 1);
assertEquals("Test department name = 'D20'", q.get(0).getName(), "D20");
} catch (Exception ex) {
assertError(ex, QueryTimeoutException.class);
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em2.getTransaction().begin();
map.clear();
map.put("javax.persistence.lock.timeout", lockWaitTime);
// Lock Emplyee(2), no department should be locked
em2.find(Employee.class, 2, LockModeType.PESSIMISTIC_READ, map);
em1.getTransaction().begin();
query = em1.createQuery("select e from Employee e where e.id < 10 order by e.department.id").setFirstResult(1);
// Lock all selected Employees, skip the first one, i.e should lock
// Employee(2)
query.setLockMode(LockModeType.PESSIMISTIC_READ);
query.setHint("javax.persistence.query.timeout", lockWaitTime);
try {
List<?> q = query.getResultList();
fail("Unexcpected find succeeded. Should throw a PessimisticLockException.");
} catch (Exception e) {
assertError(e, PessimisticLockException.class, QueryTimeoutException.class);
} finally {
if (em1.getTransaction().isActive())
em1.getTransaction().rollback();
if (em2.getTransaction().isActive())
em2.getTransaction().rollback();
}
em1.close();
em2.close();
}
/*
* Test multiple execution of the same query with pessimistic lock.
*/
public void testRepeatedQueryWithPessimisticLocks() {
EntityManager em = emf.createEntityManager();
resetSQL();
em.getTransaction().begin();
String jpql = "select e.firstName from Employee e where e.id = 1";
Query q1 = em.createQuery(jpql);
q1.setLockMode(LockModeType.PESSIMISTIC_WRITE);
String firstName1 = (String) q1.getSingleResult();
//Expected sql for Derby is:
//SELECT t0.firstName FROM Employee t0 WHERE (t0.id = CAST(? AS BIGINT)) FOR UPDATE WITH RR
String SQL1 = getLastSQL(sql);
// run the second time
resetSQL();
Query q2 = em.createQuery(jpql);
q2.setLockMode(LockModeType.PESSIMISTIC_WRITE);
String firstName2 = (String) q2.getSingleResult();
String SQL2 = getLastSQL(sql);
assertEquals(SQL1, SQL2);
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 ||
dict instanceof InformixDictionary)) {
return;
}
// Informix currently requires the lock timeout to be set directly on the dictionary
if (dict instanceof InformixDictionary) {
InformixDictionary ifxDict = (InformixDictionary)((JDBCConfiguration)emf.getConfiguration()).getDBDictionaryInstance();
ifxDict.lockModeEnabled = true;
ifxDict.lockWaitSeconds = 5;
}
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<Boolean> 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<String,Object> props = new HashMap<String,Object>();
// 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). On Informix,
// the dictionary level timeout (set above) will be used.
if (!(dict instanceof InformixDictionary)) {
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.
*
* @param actual
* exception being thrown
* @param expeceted
* type of the exception
*/
void assertError(Throwable actual, Class<? extends Throwable> ... expected) {
boolean matched = false;
String expectedNames = "";
for (Class<? extends Throwable> aExpected : expected) {
expectedNames += aExpected.getName() + ", ";
if (aExpected.isAssignableFrom(actual.getClass())) {
matched = true;
}
}
if (!matched) {
actual.printStackTrace();
throw new AssertionFailedError(actual.getClass().getName()
+ " was raised but expecting one of the following: ["
+ expectedNames.substring(0, expectedNames.length() - 2) + "]");
}
Object failed = getFailedObject(actual);
assertNotNull("Failed object is null", failed);
assertNotEquals("null", failed);
}
Object getFailedObject(Throwable e) {
if (e instanceof LockTimeoutException) {
return ((LockTimeoutException) e).getObject();
}
if (e instanceof PessimisticLockException) {
return ((PessimisticLockException) e).getEntity();
}
if (e instanceof QueryTimeoutException) {
return ((QueryTimeoutException) e).getQuery();
}
if (e instanceof OpenJPAException) {
return ((OpenJPAException) e).getFailedObject();
}
return null;
}
/**
* Separate execution thread used to forcing a lock condition on
* a row in the VersionEntity table.
*/
public class RefreshWithLock implements Callable<Boolean> {
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;
}
}
}