/* * eXist Open Source Native XML Database * Copyright (C) 2001-07 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * $Id$ */ package org.exist.storage.lock; import static org.junit.Assert.*; import java.io.IOException; import java.nio.file.Path; import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.exist.EXistException; import org.exist.TestDataGenerator; import org.exist.TestUtils; import org.exist.collections.Collection; import org.exist.collections.CollectionConfigurationException; import org.exist.collections.CollectionConfigurationManager; import org.exist.collections.IndexInfo; import org.exist.security.PermissionDeniedException; import org.exist.storage.BrokerPool; import org.exist.storage.DBBroker; import org.exist.storage.txn.TransactionManager; import org.exist.storage.txn.Txn; import org.exist.test.TestConstants; import org.exist.util.Configuration; import org.exist.util.DatabaseConfigurationException; import org.exist.util.LockException; import org.exist.xmldb.DatabaseInstanceManager; import org.exist.xmldb.XPathQueryServiceImpl; import org.exist.xmldb.XmldbURI; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.junit.After; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xmldb.api.DatabaseManager; import org.xmldb.api.base.Database; import org.xmldb.api.base.ResourceSet; import org.xmldb.api.base.XMLDBException; import org.xmldb.api.base.Resource; import org.xmldb.api.modules.CollectionManagementService; /** * Test deadlock detection and resolution. * * @author wolf */ @RunWith(Parameterized.class) public class DeadlockTest { /** pick a set of random collections to query */ private static final int TEST_RANDOM_COLLECTION = 0; /** pick a single collection to query */ private static final int TEST_SINGLE_COLLECTION = 1; /** query the root collection */ private static final int TEST_ALL_COLLECTIONS = 2; /** query a single document */ private static final int TEST_SINGLE_DOC = 3; /** apply a random mixture of the other modes */ private static final int TEST_MIXED = 4; private static final int TEST_REMOVE = 5; private static final int DELAY = 7000; /** Use 4 test runs, querying different collections */ @Parameters(name = "{0}") public static java.util.Collection<Object[]> data() { return Arrays.asList(new Object[][] { { "testRandomCollection", TEST_RANDOM_COLLECTION }, { "testSingleCollection", TEST_SINGLE_COLLECTION }, { "testAllCollections", TEST_ALL_COLLECTIONS }, { "testSingleDoc", TEST_SINGLE_DOC }, { "testMixed", TEST_MIXED }, { "testRemoved", TEST_REMOVE } }); } private static final int COLL_COUNT = 20; private static final int QUERY_COUNT = 1000; private static final int DOC_COUNT = 70; private static final int REMOVE_COUNT = 50; private static final int N_THREADS = 40; private final static String COLLECTION_CONFIG = "<collection xmlns=\"http://exist-db.org/collection-config/1.0\">" + " <index>" + " <lucene>" + " <text match='/*'/>" + " </lucene>" + " <create path=\"//section/@id\" type=\"xs:string\"/>" + " </index>" + "</collection>"; private final static String generateXQ = "<book id=\"{$filename}\" n=\"{$count}\">" + " <chapter>" + " <title>{pt:random-text(7)}</title>" + " {" + " for $section in 1 to 8 return" + " <section id=\"sect{$section}\">" + " <title>{pt:random-text(7)}</title>" + " {" + " for $para in 1 to 10 return" + " <para>{pt:random-text(40)}</para>" + " }" + " </section>" + " }" + " </chapter>" + "</book>"; private static BrokerPool pool; private final Random random = new Random(); @Parameter public String testName; @Parameter(value = 1) public int mode; @BeforeClass public static void startDB() throws DatabaseConfigurationException, EXistException, PermissionDeniedException, IOException, SAXException, CollectionConfigurationException, LockException, ClassNotFoundException, IllegalAccessException, InstantiationException, XMLDBException { final Configuration config = new Configuration(); BrokerPool.configure(1, 40, config); pool = BrokerPool.getInstance(); final TransactionManager transact = pool.getTransactionManager(); try(final DBBroker broker = pool.get(Optional.of(pool.getSecurityManager().getSystemSubject())); final Txn transaction = transact.beginTransaction();) { final Collection root = broker.getOrCreateCollection(transaction, XmldbURI.ROOT_COLLECTION_URI); assertNotNull(root); broker.saveCollection(transaction, root); final Collection test = broker.getOrCreateCollection(transaction, TestConstants.TEST_COLLECTION_URI); assertNotNull(test); broker.saveCollection(transaction, test); final CollectionConfigurationManager mgr = pool.getConfigurationManager(); mgr.addConfiguration(transaction, broker, test, COLLECTION_CONFIG); final InputSource is = new InputSource(TestUtils.resolveShakespeareSample("hamlet.xml").toUri().toASCIIString()); assertNotNull(is); final IndexInfo info = test.validateXMLResource(transaction, broker, XmldbURI.create("hamlet.xml"), is); assertNotNull(info); test.store(transaction, broker, info, is); transact.commit(transaction); // initialize XML:DB driver final Class<?> cl = Class.forName("org.exist.xmldb.DatabaseImpl"); final Database database = (Database) cl.newInstance(); DatabaseManager.registerDatabase(database); } } @After public void clearDB() throws XMLDBException { final org.xmldb.api.base.Collection root = DatabaseManager.getCollection("xmldb:exist:///db/test", "admin", ""); CollectionManagementService service = (CollectionManagementService) root.getService("CollectionManagementService", "1.0"); service.removeCollection("."); } @AfterClass public static void stopDB() throws XMLDBException { org.xmldb.api.base.Collection root = DatabaseManager.getCollection( "xmldb:exist:///db", "admin", null); DatabaseInstanceManager dim = (DatabaseInstanceManager) root.getService("DatabaseInstanceManager", "1.0"); dim.shutdown(); pool = null; } @Test public void runTasks() { final ExecutorService executor = Executors.newFixedThreadPool(N_THREADS); executor.submit(new StoreTask("store", COLL_COUNT, DOC_COUNT)); synchronized (this) { try { wait(DELAY); } catch (InterruptedException e) { e.printStackTrace(); } } for (int i = 0; i < QUERY_COUNT; i++) { executor.submit(new QueryTask(COLL_COUNT)); } if (mode == TEST_REMOVE) { for (int i = 0; i < REMOVE_COUNT; i++) { executor.submit(new RemoveDocumentTask(COLL_COUNT, DOC_COUNT)); } } executor.shutdown(); boolean terminated = false; try { terminated = executor.awaitTermination(60 * 60, TimeUnit.SECONDS); } catch (InterruptedException e) { } assertTrue(terminated); } private static class StoreTask implements Runnable { @SuppressWarnings("unused") private final String id; private final int docCount; private final int collectionCount; public StoreTask(final String id, final int collectionCount, final int docCount) { this.id = id; this.collectionCount = collectionCount; this.docCount = docCount; } @Override public void run() { final TransactionManager transact = pool.getTransactionManager(); try(final DBBroker broker = pool.get(Optional.of(pool.getSecurityManager().getSystemSubject()))) { final TestDataGenerator generator = new TestDataGenerator("xdb", docCount); Collection coll; int fileCount = 0; for (int i = 0; i < collectionCount; i++) { try(final Txn transaction = transact.beginTransaction()) { coll = broker.getOrCreateCollection(transaction, TestConstants.TEST_COLLECTION_URI.append(Integer .toString(i))); assertNotNull(coll); broker.saveCollection(transaction, coll); transact.commit(transaction); } final Path[] files = generator.generate(broker, coll, generateXQ); for (int j = 0; j < files.length; j++, fileCount++) { try(final Txn transaction = transact.beginTransaction()) { InputSource is = new InputSource(files[j].toUri() .toASCIIString()); assertNotNull(is); IndexInfo info = coll.validateXMLResource(transaction, broker, XmldbURI.create("test" + fileCount + ".xml"), is); assertNotNull(info); coll.store(transaction, broker, info, is); transact.commit(transaction); } } generator.releaseAll(); } } catch (Exception e) { e.printStackTrace(); // fail(e.getMessage()); } } } private class QueryTask implements Runnable { private int collectionCount; public QueryTask(int collectionCount) { this.collectionCount = collectionCount; } public void run() { final StringBuilder buf = new StringBuilder(); String collection = "/db"; int currentMode = mode; if (mode == TEST_MIXED || currentMode == TEST_REMOVE) currentMode = random.nextInt(4); if (currentMode == TEST_SINGLE_COLLECTION) { int collectionId = random.nextInt(collectionCount); collection = "/db/test/" + collectionId; buf.append("collection('").append(collection) .append("')//chapter/section[@id = 'sect1']"); } else if (currentMode == TEST_RANDOM_COLLECTION) { List<Integer> collIds = new ArrayList<Integer>(7); for (int i = 0; i < 3; i++) { int r; do { r = random.nextInt(collectionCount); } while (collIds.contains(r)); collIds.add(r); } buf.append("("); for (int i = 0; i < 3; i++) { if (i > 0) buf.append(", "); buf.append("collection('/db/test/").append(collIds.get(i)) .append("')"); } buf.append(")//chapter/section[@id = 'sect1']"); collection = "/db/test"; } else if (currentMode == TEST_SINGLE_DOC) { int collectionId = random.nextInt(collectionCount); collection = "/db/test/" + collectionId; buf.append("doc('").append(collection).append("/test1.xml')//chapter/section[@id = 'sect1']"); } else { buf.append("//chapter/section[@id = 'sect1']"); } String query = buf.toString(); try { org.xmldb.api.base.Collection testCollection = DatabaseManager .getCollection("xmldb:exist://" + collection, "admin", null); if (testCollection == null) return; XPathQueryServiceImpl service = (XPathQueryServiceImpl) testCollection .getService("XQueryService", "1.0"); service.beginProtected(); try { ResourceSet result = service.query(query); result.getSize(); } finally { service.endProtected(); } } catch (Exception e) { e.printStackTrace(); fail(e.getMessage()); } } } private class RemoveDocumentTask implements Runnable { private final int collectionCount; private final int documentCount; public RemoveDocumentTask(final int collectionCount, final int documentCount) { this.collectionCount = collectionCount; this.documentCount = documentCount; } @Override public void run() { boolean removed = false; do { final int collectionId = random.nextInt(collectionCount); final String collection = "/db/test/" + collectionId; final int docId = random.nextInt(documentCount) * collectionId; final String document = "test" + docId + ".xml"; try { final org.xmldb.api.base.Collection testCollection = DatabaseManager.getCollection("xmldb:exist://" + collection, "admin", ""); final Resource resource = testCollection.getResource(document); if (resource != null) { testCollection.removeResource(resource); removed = true; } } catch (final XMLDBException e) { e.printStackTrace(); fail(e.getMessage()); } } while (!removed); } } }