/* * Copyright 2009 Udai Gupta, Ralf Joachim * * Licensed 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.castor.cpa.test.test02; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.castor.cpa.test.framework.CPATestCase; import org.castor.cpa.test.framework.xml.types.DatabaseEngineType; import org.exolab.castor.jdo.Database; import org.exolab.castor.jdo.JDOManager; import org.exolab.castor.jdo.OQLQuery; import org.exolab.castor.jdo.ObjectModifiedException; import org.exolab.castor.jdo.PersistenceException; import org.exolab.castor.jdo.QueryResults; import org.exolab.castor.mapping.AccessMode; /** * Concurrent access test. Tests a JDO modification and concurrent * JDBC modification to determine if JDO can detect the modification * with dirty checking. */ public final class TestConcurrent extends CPATestCase { public static final String JDBC_VALUE = "jdbc value"; public static final String JDO_VALUE = "jdo value"; public static final int WAIT_FOR_CONCURRENT_UPDATE = 2000; /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta * Commons Logging</a> instance used for all logging. */ private static final Log LOG = LogFactory.getLog(TestConcurrent.class); private static final String DBNAME = "test02"; private static final String MAPPING = "/org/castor/cpa/test/test02/mapping.xml"; private Database _db; /** * Constructor */ public TestConcurrent(final String name) { super(name); } // Test are only included/excluded for engines that have been tested with this test suite. public boolean include(final DatabaseEngineType engine) { return (engine == DatabaseEngineType.MYSQL) || (engine == DatabaseEngineType.ORACLE) || (engine == DatabaseEngineType.POSTGRESQL); } // Test fails on DERBY even if isolation levels and locking are supported // HSQL does not support isolation levels and locking public boolean exclude(final DatabaseEngineType engine) { return (engine == DatabaseEngineType.DERBY) || (engine == DatabaseEngineType.HSQL); } /** * Initializes fields */ public void setUp() throws Exception { _db = getJDOManager(DBNAME, MAPPING).getDatabase(); } /** * Close the database and JDBC connection */ public void tearDown() throws PersistenceException, SQLException { if (_db.isActive()) { _db.rollback(); } _db.close(); } /** * Test for concurrent modification detection in Shared Mode. * (Optimistic Locking Mode) * This test contains two parts. The first part tests if Castor * JDO can detect concurrent modification done directly via * JDBC in Shared Mode. The second part test if Castor can * ignores concurrent modification done to fields that * indicates dirty check should not be done. */ public void testAccessModeShared() throws Exception { LOG.info("Running in access mode shared"); // part 1 runDirtyChecked(Database.SHARED); // part 2 runDirtyIgnored(Database.SHARED); } /** * Test for concurrent modification detection in Exclusive Mode. * (Pessimistic Locking Mode) * This test contains two parts. The first part tests if Castor * JDO can detect concurrent modification done directly via * JDBC in Shared Mode. The second part tests if Castor can * ignore concurrent modification done to fields that * are indicated dirty check should not be done. */ public void testAccessModeExclusive() throws Exception { LOG.info("Running in access mode exclusive"); // part 1 runDirtyChecked(Database.EXCLUSIVE); // part 2 runDirtyIgnored(Database.EXCLUSIVE); } /** * Test for concurrent modification detection in DbLocked Mode. * (Pessimistic Locking Mode plus database row lock) * This test contains two parts. The first part tests if Castor * JDO can detect concurrent modification done directly via * JDBC in Shared Mode. The second part tests if Castor can * ignores concurrent modification done to fields that are * indicated dirty check should not be done. * (note: some databases don't support database lock and will * fails this test case) */ public void testAccessModeDbLocked() throws Exception { LOG.info("Running in access mode db-locked"); // part 1 runDirtyChecked(Database.DBLOCKED); // part 2 runDirtyIgnored(Database.DBLOCKED); } /** * This method is called by the tests and preform the actual * concurrent modification test. * * @param accessMode the access mode that is used in the concurrent * modification tests */ private void runDirtyChecked(final AccessMode accessMode) throws Exception { JDOManager jdo = getJDOManager(DBNAME, MAPPING); OQLQuery oql; Sample object; QueryResults enumeration; // Open transaction in order to perform JDO operations _db.begin(); // Determine if test object exists, if not create it. // If it exists, set the name to some predefined value // that this test will later override. oql = _db.getOQLQuery("SELECT object FROM " + Sample.class.getName() + " object WHERE id = $1"); oql.bind(Sample.DEFAULT_ID); enumeration = oql.execute(); if (enumeration.hasMore()) { object = (Sample) enumeration.next(); LOG.debug("Retrieved object: " + object); object.setValue1(Sample.DEFAULT_VALUE_1); object.setValue2(Sample.DEFAULT_VALUE_2); } else { object = new Sample(); LOG.debug("Creating new object: " + object); _db.create(object); } _db.commit(); // Open a new transaction in order to conduct test _db.begin(); oql.bind(new Integer(Sample.DEFAULT_ID)); object = (Sample) oql.execute(accessMode).nextElement(); object.setValue1(JDO_VALUE); // Perform direct JDBC access and override the value of that table if (accessMode != Database.DBLOCKED) { Connection conn = jdo.getConnectionFactory().createConnection(); Statement stmt = conn.createStatement(); stmt.execute("UPDATE test02_sample SET value1='" + JDBC_VALUE + "' " + "WHERE id=" + Sample.DEFAULT_ID); stmt.close(); conn.close(); LOG.debug("OK: Updated object from JDBC"); } else { Thread th = new ConcurrentUpdateThread(jdo); th.start(); synchronized (this) { try { wait(WAIT_FOR_CONCURRENT_UPDATE); if (th.isAlive()) { th.interrupt(); LOG.debug("OK: Cannot update object from JDBC"); } else { LOG.error("Error: Updated object from JDBC"); fail("Updated test object from JDBC"); } } catch (InterruptedException ex) { } } } // Commit JDO transaction, this should report object modified exception LOG.debug("Committing JDO update: dirty checking field modified"); if (accessMode != Database.DBLOCKED) { try { _db.commit(); LOG.error("Error: ObjectModifiedException not thrown"); fail("ObjectModifiedException not thrown"); } catch (ObjectModifiedException ex) { LOG.debug("OK: ObjectModifiedException thrown"); } } else { try { _db.commit(); LOG.debug("OK: ObjectModifiedException not thrown"); // After _db.commit the concurrent update will be performed. // and we need to undo it. Connection conn = jdo.getConnectionFactory().createConnection(); Statement stmt = conn.createStatement(); stmt.execute("UPDATE test02_sample SET value1='" + JDO_VALUE + "' " + "WHERE id=" + Sample.DEFAULT_ID); stmt.close(); conn.close(); } catch (ObjectModifiedException ex) { _db.rollback(); LOG.error("Error: ObjectModifiedException thrown"); fail("ObjectModifiedException not thrown"); } } } /** * This method is called by the tests and preform the actual * concurrent modification test. * * @param accessMode the access mode that is used in the concurrent * modification tests */ private void runDirtyIgnored(final AccessMode accessMode) throws Exception { JDOManager jdo = getJDOManager(DBNAME, MAPPING); Sample object; // Open transaction in order to perform JDO operations _db.begin(); // Determine if test object exists, if not create it. // If it exists, set the name to some predefined value // that this test will later override. OQLQuery oql = _db.getOQLQuery("SELECT object FROM " + Sample.class.getName() + " object WHERE id = $1"); oql.bind(Sample.DEFAULT_ID); QueryResults enumeration = oql.execute(); if (enumeration.hasMore()) { object = (Sample) enumeration.next(); LOG.debug("Retrieved object: " + object); object.setValue1(Sample.DEFAULT_VALUE_1); object.setValue2(Sample.DEFAULT_VALUE_2); } else { object = new Sample(); LOG.debug("Creating new object: " + object); _db.create(object); } _db.commit(); // Open a new transaction in order to conduct test _db.begin(); oql.bind(new Integer(Sample.DEFAULT_ID)); object = (Sample) oql.execute(accessMode).nextElement(); object.setValue2(JDO_VALUE); // Perform direct JDBC access and override the value of that table if (accessMode != Database.DBLOCKED) { Connection conn = jdo.getConnectionFactory().createConnection(); Statement stmt = conn.createStatement(); stmt.execute("UPDATE test02_sample SET value2='" + JDBC_VALUE + "' " + "WHERE id=" + Sample.DEFAULT_ID); stmt.close(); conn.close(); LOG.debug("Updated test object from JDBC"); } // Commit JDO transaction, this should report object modified exception LOG.debug("Commit update: no dirty checking field not modified"); try { _db.commit(); LOG.debug("OK: ObjectModifiedException not thrown"); } catch (ObjectModifiedException ex) { if (_db.isActive()) { _db.rollback(); } LOG.error("Error: ObjectModifiedException thrown", ex); fail("ObjectModifiedException thrown"); } } }