/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Oct 3, 2007 */ package com.bigdata.journal; import java.nio.channels.FileChannel; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import com.bigdata.btree.BTree; import com.bigdata.btree.IIndex; import com.bigdata.btree.ITupleIterator; import com.bigdata.btree.IndexMetadata; import com.bigdata.btree.keys.KeyBuilder; import com.bigdata.journal.AbstractInterruptsTestCase.InterruptMyselfTask; import com.bigdata.journal.AbstractTask.ResubmitException; import com.bigdata.journal.ConcurrencyManager.Options; import com.bigdata.service.DataService; import com.bigdata.service.ndx.DataServiceTupleIterator; import com.bigdata.util.DaemonThreadFactory; /** * Test suite for the {@link IConcurrencyManager} interface on the * {@link Journal}. * * @todo write test cases that submit various kinds of operations and verify the * correctness of those individual operations. refactor the services * package to do this, including things such as the * {@link DataServiceTupleIterator}. this will help to isolate the * correctness of the data service "api", including concurrency of * operations, from the {@link DataService}. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> */ public class TestConcurrentJournal extends ProxyTestCase<Journal> { public TestConcurrentJournal() { super(); } public TestConcurrentJournal(String name) { super(name); } /** * Test ability to create a {@link Journal} and then shut it down (in * particular this is testing shutdown of the thread pool on the * {@link ConcurrencyManager}). */ public void test_shutdown() { final Journal journal = new Journal(getProperties()); try { journal.shutdown(); } finally { journal.destroy(); } } public void test_shutdownNow() { final Journal journal = new Journal(getProperties()); try { journal.shutdownNow(); } finally { journal.destroy(); } } /** * Submits an unisolated task to the read service and verifies that it executes. * * @throws InterruptedException * @throws ExecutionException */ public void test_submit_readService_01() throws InterruptedException, ExecutionException { final Journal journal = new Journal(getProperties()); try { final long commitCounterBefore = journal.getRootBlockView() .getCommitCounter(); final String resource = "foo"; final AtomicBoolean ran = new AtomicBoolean(false); final Future<String> future = journal.submit(new AbstractTask<String>(journal, ITx.READ_COMMITTED, resource) { /** * The task just sets a boolean value and returns the name of * the sole resource. It does not actually read anything. */ @Override protected String doTask() throws Exception { ran.compareAndSet(false, true); return getOnlyResource(); } }); // the test task returns the resource as its value. assertEquals("result", resource, future.get()); /* * make sure that the flag was set (not reliably set until we get() * the future). */ assertTrue("ran", ran.get()); /* * Verify that a commit was NOT performed. */ assertEquals("commit counter changed?", commitCounterBefore, journal.getRootBlockView().getCommitCounter()); } finally { journal.destroy(); } } /** * Submits an unisolated task to the write service and verifies that it * executes. * * @throws InterruptedException * @throws ExecutionException */ public void test_submit_writeService_01() throws InterruptedException, ExecutionException { final Journal journal = new Journal(getProperties()); try { final String resource = "foo"; final long commitCounterBefore = journal.getRootBlockView() .getCommitCounter(); final AtomicBoolean ran = new AtomicBoolean(false); final Future<String> future = journal.submit(new AbstractTask<String>(journal, ITx.UNISOLATED, resource) { /** * The task just sets a boolean value and returns the name of * the sole resource. It does not actually read or write on * anything. */ @Override protected String doTask() throws Exception { ran.compareAndSet(false, true); return getOnlyResource(); } }); // the test task returns the resource as its value. assertEquals("result", resource, future.get()); /* * make sure that the flag was set (not reliably set until we get() * the future). */ assertTrue("ran", ran.get()); /* * Verify that the commit counter was changed. */ assertEquals("commit counter unchanged?", commitCounterBefore + 1, journal.getRootBlockView().getCommitCounter()); } finally { journal.destroy(); } } /** * Submits an read-only task to the transaction service and verifies that it * executes. * * @todo this test is somewhat odd since there are no commits on the journal * when we request a read-only transaction. Potentially that should be * illegal since there is nothing available to read. Certainly the * returned transaction identifier MUST NOT be ZERO (0) since that * would indicate an unisolated operation! * * @throws InterruptedException * @throws ExecutionException */ public void test_submit_txService_readOnly_01() throws InterruptedException, ExecutionException { final Journal journal = new Journal(getProperties()); try { final String resource = "foo"; final long commitCounterBefore = journal.getRootBlockView() .getCommitCounter(); final AtomicBoolean ran = new AtomicBoolean(false); final long tx = journal.newTx(ITx.READ_COMMITTED); assertNotSame(ITx.UNISOLATED, tx); final Future<String> future = journal.submit(new AbstractTask<String>(journal, tx, resource) { /** * The task just sets a boolean value and returns the name of * the sole resource. It does not actually read or write on * anything. */ @Override protected String doTask() throws Exception { ran.compareAndSet(false, true); return getOnlyResource(); } }); // the test task returns the resource as its value. assertEquals("result", resource, future.get()); /* * make sure that the flag was set (not reliably set until we get() * the future). */ assertTrue("ran", ran.get()); /* * Verify that the commit counter was NOT changed. */ assertEquals("commit counter changed?", commitCounterBefore, journal.getRootBlockView().getCommitCounter()); // commit of a read-only tx returns commitTime of ZERO(0L). assertEquals(0L, journal.commit(tx)); } finally { journal.destroy(); } } /** * Submits a read-committed task to the transaction service and verifies * that it executes. * * @throws InterruptedException * @throws ExecutionException */ public void test_submit_txService_readCommitted_01() throws InterruptedException, ExecutionException { final Journal journal = new Journal(getProperties()); try { final String resource = "foo"; final long commitCounterBefore = journal.getRootBlockView() .getCommitCounter(); final AtomicBoolean ran = new AtomicBoolean(false); final long tx = ITx.READ_COMMITTED; // final long tx = journal.newTx(IsolationEnum.ReadCommitted); // assertNotSame(ITx.UNISOLATED, tx); final Future<String> future = journal.submit(new AbstractTask<String>(journal, tx, resource) { /** * The task just sets a boolean value and returns the name of * the sole resource. It does not actually read or write on * anything. */ @Override protected String doTask() throws Exception { ran.compareAndSet(false, true); return getOnlyResource(); } }); // the test task returns the resource as its value. assertEquals("result", resource, future.get()); /* * make sure that the flag was set (not reliably set until we get() * the future). */ assertTrue("ran", ran.get()); /* * Verify that the commit counter was NOT changed. */ assertEquals("commit counter changed?", commitCounterBefore, journal.getRootBlockView().getCommitCounter()); // // commit of a readCommitted tx returns commitTime of ZERO(0L). // assertEquals(0L,journal.commit(tx)); // should be illegal since this is not a full transaction. try { journal.abort(tx); fail("Expecting: " + IllegalStateException.class); } catch (IllegalStateException ex) { log.info("Ignoring expected exception: " + ex); } } finally { journal.destroy(); } } /** * Submits a read-write task with an empty write set to the transaction * service and verifies that it executes. * * @throws InterruptedException * @throws ExecutionException */ public void test_submit_txService_readWrite_01() throws InterruptedException, ExecutionException { final Journal journal = new Journal(getProperties()); try { final String resource = "foo"; final long commitCounterBefore = journal.getRootBlockView() .getCommitCounter(); final AtomicBoolean ran = new AtomicBoolean(false); final long tx = journal.newTx(ITx.UNISOLATED); assertNotSame(ITx.UNISOLATED, tx); final Future<String> future = journal.submit(new AbstractTask<String>(journal, tx, resource) { /** * The task just sets a boolean value and returns the name of * the sole resource. It does not actually read or write on * anything. */ @Override protected String doTask() throws Exception { ran.compareAndSet(false, true); return getOnlyResource(); } }); // the test task returns the resource as its value. assertEquals("result", resource, future.get()); /* * make sure that the flag was set (not reliably set until we get() * the future). */ assertTrue("ran", ran.get()); /* * Verify that the commit counter was NOT changed. */ assertEquals("commit counter changed?", commitCounterBefore, journal.getRootBlockView().getCommitCounter()); // commit of a readWrite tx with an empty result set returns // commitTime // of ZERO(0L). assertEquals(0L, journal.commit(tx)); } finally { journal.destroy(); } } /** * Submits an unisolated task to the write service. The task just sleeps. We * then verify that we can interrupt that task using * {@link Future#cancel(boolean)} with * <code>mayInterruptWhileRunning := true</code> and that an appropriate * exception is thrown in the main thread. * * @throws InterruptedException * @throws ExecutionException */ public void test_submit_interrupt01() throws InterruptedException, ExecutionException { final Properties properties = getProperties(); final Journal journal = new Journal(properties); try { // Note: properties.setProperty(Options.SHUTDOWN_TIMEOUT, "500"); final String[] resource = new String[] { "foo" }; final long commitCounterBefore = journal.getRootBlockView() .getCommitCounter(); final AtomicBoolean ran = new AtomicBoolean(false); final Future<Void> future = journal.submit(new AbstractTask<Void>(journal, ITx.UNISOLATED, resource) { /** * The task just sets a boolean value and then sleeps. */ @Override protected Void doTask() throws Exception { ran.compareAndSet(false, true); while (true) { if (Thread.interrupted()) { if (log.isInfoEnabled()) log.info("Interrupted."); /* * Note: If you simply continue processing rather * than throwing an exception then the interrupt is * _ignored_. */ throw new InterruptedException( "Task was interrupted"); } /* * Note: this will notice if the Thread is interrupted. */ Thread.sleep(Long.MAX_VALUE); } } }); // wait until the task starts executing. while (true) { if (ran.get()) break; Thread.sleep(100); } // interrupts and cancels the task. assertTrue(future.cancel(true/* mayInterruptWhileRunning */)); // the task was cancelled. assertTrue(future.isCancelled()); // verify that get() throws the expected exception. try { future.get(); fail("Expecting: " + CancellationException.class); } catch (CancellationException ex) { if (log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex); } /* * Verify that the commit counter was changed. */ assertEquals("commit counter changed?", commitCounterBefore, journal.getRootBlockView().getCommitCounter()); } finally { journal.destroy(); } } /** * Submits an unisolated task to the write service. The task just sleeps. We * then verify that we can terminate that task using * {@link Future#cancel(boolean)} with * <code>mayInterruptWhileRunning := false</code> and that an appropriate * exception is thrown in the main thread when we {@link Future#get()} the * result of the task. * <p> * Note: {@link FutureTask#cancel(boolean)} is able to return control to the * caller without being allowed to interrupt the task. However, it does NOT * terminate the task - the worker thread is still running. Once the main * thread reaches {@link Journal#shutdown()} it awaits the termination of * the {@link WriteExecutorService}. However that service does NOT * terminate because that worker thread is still running an infinite loop. * <p> * Eventually, we interrupt the thread directly using a reference to it we * obtained when setting up the task. This allows the journal to terminate * and the test to complete. * * @throws InterruptedException * @throws ExecutionException */ public void test_submit_interrupt02() throws InterruptedException, ExecutionException { final Properties properties = getProperties(); properties.setProperty(Options.SHUTDOWN_TIMEOUT, "500"); final Journal journal = new Journal(properties); // the thread that we need to eventually interrupt. final AtomicReference<Thread> t = new AtomicReference<Thread>(null); try { final String[] resource = new String[] { "foo" }; final long commitCounterBefore = journal.getRootBlockView() .getCommitCounter(); final AtomicBoolean ran = new AtomicBoolean(false); final Future<Void> future = journal.submit(new AbstractTask<Void>(journal, ITx.UNISOLATED, resource) { /** * The task just sets a boolean value and then runs an infinite * loop, <strong>ignoring interrupts</strong>. */ @Override protected Void doTask() throws Exception { t.set(Thread.currentThread()); ran.compareAndSet(false, true); while (true) { try { Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException ex) { /* ignore */ if (log.isInfoEnabled()) log.info("Ignoring interrupt: " + ex); break; } } return null; } }); // wait until the task starts executing. while (true) { if (ran.get()) break; Thread.sleep(100); } // we have a reference to the thread running the task. assertNotNull(t.get()); // this terminates the task, but does NOT interrupt it. assertTrue(future.cancel(false/* mayInterruptWhileRunning */)); // the task was cancelled. assertTrue(future.isCancelled()); // verify that get() throws the expected exception. try { future.get(); fail("Expecting: " + CancellationException.class); } catch (CancellationException ex) { if (log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex); } /* * Verify that the commit counter was NOT changed. */ assertEquals("commit counter changed?", commitCounterBefore, journal.getRootBlockView().getCommitCounter()); // Verify that the thread was not interupted. assertFalse(t.get().isInterrupted()); // Sleep a bit Thread.sleep(250); // Verify that the thread is not interrupted. assertFalse(t.get().isInterrupted()); // Interrupt the thread so the Journal will terminate. t.get().interrupt(); } finally { journal.destroy(); } } ///* // * Note: This test no longer matches new semantics for the // * {@link WriteExecutorService}. A failed unisolated task will now return as // * soon as we rollback the indices to their last checkpoint (which we do just by // * closing the index). // * // */ // /** // * This task verifies that an abort will wait until all running tasks // * complete (ie, join the "abort group") before aborting. // * <p> // * If the abort occurs while task(s) are still running then actions by those // * tasks can break the concurrency control mechanisms. For example, writes // * on unisolated indices by tasks in the abort group could be made restart // * safe with the next commit. However, the lock system SHOULD still keep new // * tasks from writing on resources locked by tasks that have not yet // * terminated. // */ // public void test_submit_interrupt03() throws InterruptedException, ExecutionException { // // Properties properties = getProperties(); // // Journal journal = new Journal(properties); // // try { // // final String[] resource = new String[] { "foo" }; // // { // journal.registerIndex(resource[0]); // journal.commit(); // } // // final long commitCounterBefore = journal.getRootBlockView() // .getCommitCounter(); // // /* // * 4 - done. // */ // final AtomicInteger runState = new AtomicInteger(0); // // final Lock lock = new ReentrantLock(); // // /** // * Used to force an abort. // */ // class PrivateException extends RuntimeException { // // private static final long serialVersionUID = 1L; // // PrivateException(String msg) { // super(msg); // } // // } // // Future<Object> f1 = journal.submit(new AbstractTask(journal, // ITx.UNISOLATED, resource) { // // /** // */ // protected Object doTask() throws Exception { // // runState.compareAndSet(0, 1); // // // low-level write so there is some data to be committed. // getJournal().write(getRandomData()); // // // wait more before exiting. // { // final long begin = System.currentTimeMillis(); // final long timeout = 5000; // ms // log.warn("Sleeping " + timeout + "ms"); // try { // final long elapsed = System.currentTimeMillis() // - begin; // Thread.sleep(timeout - elapsed/* ms */); // } catch (InterruptedException ex) { // log.warn(ex); // } // } // // lock.lock(); // try { // runState.compareAndSet(1, 4); // log.warn("Done"); // } finally { // lock.unlock(); // } // // return null; // // } // // }); // // // force async abort of the commit group. // // Future<Object> f2 = journal.submit(new AbstractTask(journal, // ITx.UNISOLATED, new String[] {}) { // // protected Object doTask() throws Exception { // // log // .warn("Running task that will force abort of the commit group."); // // // low-level write. // getJournal().write(getRandomData()); // // throw new PrivateException( // "Forcing abort of the commit group."); // // } // // }); // // // Verify that the commit counter was NOT changed. // assertEquals("commit counter changed?", commitCounterBefore, // journal.getRootBlockView().getCommitCounter()); // // /* // * Verify that the write service waits for the interrupted task to // * join the abort group. // */ // log.warn("Waiting for 1st task to finish"); // while (runState.get() < 4) { // // lock.lock(); // try { // if (runState.get() < 4) { // // No abort yet. // assertEquals("Not expecting abort", 0, journal // .getConcurrencyManager().writeService // .getAbortCount()); // } // } finally { // lock.unlock(); // } // Thread.sleep(20); // // } // log.warn("Reached runState=" + runState); // // // wait for the abort or a timeout. // { // log.warn("Waiting for the abort."); // final long begin = System.currentTimeMillis(); // while ((begin - System.currentTimeMillis()) < 1000) { // if (journal.getConcurrencyManager().writeService // .getAbortCount() > 0) { // log.warn("Noticed abort"); // break; // } else { // // Verify that the commit counter was NOT changed. // assertEquals("commit counter changed?", // commitCounterBefore, journal.getRootBlockView() // .getCommitCounter()); // } // } // } // // // verify did abort. // assertEquals("Expecting abort", 1, // journal.getConcurrencyManager().writeService // .getAbortCount()); // // /* // * Verify that both futures are complete and that both throw // * exceptions since neither task was allowed to commit. // * // * Note: f1 completed successfully but the commit group was // * discarded when f2 threw an exception. Therefore f1 sees an inner // * RetryException while f2 sees the exception that it threw as its // * inner exception. // */ // { // // try { // f1.get(); // fail("Expecting exception."); // } catch (ExecutionException ex) { // assertTrue(isInnerCause(ex, RetryException.class)); // log.warn("Expected exception: " + ex, ex); // } // try { // f2.get(); // fail("Expecting exception."); // } catch (ExecutionException ex) { // log.warn("Expected exception: " + ex, ex); // assertTrue(isInnerCause(ex, PrivateException.class)); // } // } // // } finally { // // journal.shutdown(); // // journal.deleteResources(); // // } // // } /** * Verify that an {@link AbstractTask} correctly rejects an attempt to * submit the same instance twice. This is important since the base class * has various items of state that are not thread-safe and are not designed * to be reusable. * * @throws ExecutionException * @throws InterruptedException */ public void test_tasksAreNotThreadSafe() throws InterruptedException, ExecutionException { final Journal journal = new Journal(getProperties()); try { final String[] resource = new String[] { "foo" }; /* * Note: this task is stateless */ final AbstractTask<Void> task = new AbstractTask<Void>(journal, ITx.UNISOLATED, resource) { @Override protected Void doTask() throws Exception { return null; } }; /* * Note: We have to request the result of the task before * re-submitting the task again since the duplicate instance is * being silently dropped otherwise - I expect that there is a hash * set involved somewhere such that duplicates can not exist in the * queue. */ journal.submit(task).get(); /* * Submit the task again - it will fail. */ try { journal.submit(task).get(); fail("Expecting: " + ResubmitException.class); } catch (ExecutionException ex) { if (ex.getCause() instanceof ResubmitException) { if (log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex); } else { fail("Expecting: " + ResubmitException.class); } } } finally { journal.destroy(); } } /* * @todo revisit this unit test. It's semantics appear to have aged. */ // /** // * Test verifies that an {@link ITx#UNISOLATED} task failure does not cause // * concurrent writers to abort. The test also verifies that the // * {@link Checkpoint} record for the named index is NOT updated since none // * of the tasks write anything on the index. // * // * @todo The assumptions for this test may have been invalidated by the // * recent (4/29) changes to the group commit and task commit protocol // * and this test might need to be reworked or rewritten. // */ // public void test_writeService001() throws Exception { // // final Journal journal = new Journal(getProperties()); // // try { // // final String name = "test"; // // // Note: checkpoint for the newly registered index. // final long checkpointAddr0; // { // // journal.registerIndex(name,new IndexMetadata(name,UUID.randomUUID())); // // journal.commit(); // // checkpointAddr0 = journal.getIndex(name).getCheckpoint() // .getCheckpointAddr(); // // } // // // the list of tasks to be run. // final List<AbstractTask<Object>> tasks = new LinkedList<AbstractTask<Object>>(); // // // NOP // tasks.add(new AbstractTask(journal, ITx.UNISOLATED, name) { // protected String getTaskName() { // return "a"; // } // protected Object doTask() throws Exception { // assertEquals(checkpointAddr0, ((BTree) getIndex(name)) // .getCheckpoint().getCheckpointAddr()); // return null; // } // }); // // // throws exception. // tasks.add(new AbstractTask(journal, ITx.UNISOLATED, name) { // protected String getTaskName() { // return "b"; // } // protected Object doTask() throws Exception { // assertEquals(checkpointAddr0, ((BTree) getIndex(name)) // .getCheckpoint().getCheckpointAddr()); // throw new ForcedAbortException(); // } // }); // // // NOP // tasks.add(new AbstractTask(journal, ITx.UNISOLATED, name) { // protected String getTaskName() { // return "c"; // } // protected Object doTask() throws Exception { // assertEquals(checkpointAddr0, ((BTree) getIndex(name)) // .getCheckpoint().getCheckpointAddr()); // return null; // } // }); // // // the commit counter before we submit the tasks. // final long commitCounter0 = journal.getRootBlockView() // .getCommitCounter(); // // // the write service on which the tasks execute. // final WriteExecutorService writeService = journal // .getConcurrencyManager().getWriteService(); // // // the group commit count before we submit the tasks. // final long groupCommitCount0 = writeService.getGroupCommitCount(); // // // the abort count before we submit the tasks. // final long abortCount0 = writeService.getAbortCount(); // // // the #of failed tasks before we submit the tasks. // final long failedTaskCount0 = writeService.getTaskFailedCount(); // // // the #of successfully tasks before we submit the tasks. // final long successTaskCount0 = writeService.getTaskSuccessCount(); // // // the #of successfully committed tasks before we submit the tasks. // final long committedTaskCount0 = writeService.getTaskCommittedCount(); // // // submit the tasks and await their completion. // final List<Future<Object>> futures = journal.invokeAll( tasks ); // // /* // * verify the #of commits on the journal is unchanged since nothing // * is written by any of these tasks. // * // * The expectation is that the tasks that succeed make it into the // * same commit group while the task that throws an exception does // * not cause the commit group to be aborted. // * // * Note: The tasks will make it into the same commit group iff the // * first task that completes is willing to wait for the others to // * join the commit group. // * // * Note: The tasks have a dependency on the same resource so they // * will be serialized (executed in a strict sequence). // */ // assertEquals("commitCounter", commitCounter0, journal // .getRootBlockView().getCommitCounter()); // // // however, a group commit SHOULD have been performed. // assertEquals("groupCommitCount", groupCommitCount0 + 1, writeService // .getGroupCommitCount()); // // // NO aborts should have been performed. // assertEquals("aboutCount", abortCount0, writeService.getAbortCount()); // // // ONE(1) tasks SHOULD have failed. // assertEquals("failedTaskCount", failedTaskCount0 + 1, writeService. // getTaskFailedCount()); // // // TWO(2) tasks SHOULD have succeeded. // assertEquals("successTaskCount", successTaskCount0 + 2, writeService // .getTaskSuccessCount()); // // // TWO(2) successfull tasks SHOULD have been committed. // assertEquals("committedTaskCount", committedTaskCount0 + 2, writeService // .getTaskCommittedCount()); // // assertEquals( 3, futures.size()); // // // tasks[0] // { // // Future f = futures.get(0); // // assertTrue(f.isDone()); // // f.get(); // No exception expected. // // } // // // tasks[2] // { // // Future f = futures.get(2); // // assertTrue(f.isDone()); // // f.get(); // No exception expected. // // } // // // tasks[1] // { // // Future f = futures.get(1); // // assertTrue(f.isDone()); // // try { // f.get(); // fail("Expecting exception"); // } catch(ExecutionException ex) { // assertTrue(InnerCause.isInnerCause(ex, ForcedAbortException.class)); // } // // } // // assertEquals(checkpointAddr0, journal.getIndex(name) // .getCheckpoint().getCheckpointAddr()); // // } finally { // // journal.destroy(); // // } // // } /** * Test verifies that a write on an index will cause the index to be * checkpointed when the task completes. */ public void test_writeServiceCheckpointDirtyIndex()throws Exception { final Journal journal = new Journal(getProperties()); try { final String name = "test"; // Note: checkpoint for the newly registered index. final long checkpointAddr0; { journal.registerIndex(new IndexMetadata(name,UUID.randomUUID())); journal.commit(); checkpointAddr0 = journal.getIndex(name).getCheckpoint() .getCheckpointAddr(); } // Submit task that writes on the index and wait for the commit. journal.submit(new AbstractTask<Void>(journal,ITx.UNISOLATED,name){ @Override protected Void doTask() throws Exception { final BTree ndx = (BTree)getIndex(name); // verify checkpoint unchanged. assertEquals(checkpointAddr0,ndx.getCheckpoint().getCheckpointAddr()); // write on the index. ndx.insert(new byte[]{1}, new byte[]{1}); return null; } }).get(); // wait for the commit. // verify checkpoint was updated. assertNotSame(checkpointAddr0, journal.getIndex(name) .getCheckpoint().getCheckpointAddr()); } finally { journal.destroy(); } } /* * @todo revisit this unit test. It's semantics appear to have aged. */ // /** // * Test verifies that a task failure causes accessed indices to be rolled // * back to their last checkpoint. // * // * FIXME write test where a task registers an index and then throws an // * exception. This will cause the index to have a checkpoint record that // * does not agree with {@link Name2Addr} for the last commit point. Verify // * that the index is not in fact available to another task that is executed // * after the failed task (it will be if we merely close the index and then // * re-open it since it will reopen from the last checkpoint NOT from the // * last commit point). // * // * FIXME write test where a tasks (a), (b) and (c) are submitted with // * invokeAll() in that order and require a lock on the same index. Task (a) // * writes on an existing index and completes normally. The index SHOULD be // * checkpointed and task (b) SHOULD be able to read the data written in task // * (a) and SHOULD be run in the same commit group. Task (b) then throws an // * exception. Verify that the index is rolledback to the checkpoint for (a) // * (vs the last commit point) using task (c) which will read on the same // * index looking for the correct checkpoint record and data in the index. // * This test will fail if (b) is not reading from the checkpoint written by // * (a) or if (c) reads from the last commit point rather than the checkpoint // * written by (a). // * // * FIXME write tests to verify that an {@link #abort()} causes all running // * tasks to be interrupted and have their write sets discarded (should it? // * Should an abort just be an shutdownNow() in response to some truely nasty // * problem?) // */ // public void test_writeService002()throws Exception { // // final Properties properties = new Properties(getProperties()); // // /* // * Note: restricting the thread pool size does not give us the control // * that we need because it results in each task running as its own // * commit group. // */ //// /* //// * Note: Force the write service to be single threaded so that we can //// * control the order in which the tasks start by the order in which they //// * are submitted. //// */ //// properties.setProperty(Options.WRITE_SERVICE_CORE_POOL_SIZE,"1"); //// properties.setProperty(Options.WRITE_SERVICE_MAXIMUM_POOL_SIZE,"1"); // // final Journal journal = new Journal(properties); // // try { // // final String name = "test"; // // // Note: checkpoint for the newly registered index. // final long checkpointAddr0; // { // // // register // journal.registerIndex(name); // // // commit. // journal.commit(); // // // note checkpoint for index. // checkpointAddr0 = journal.getIndex(name).getCheckpoint() // .getCheckpointAddr(); // // } // // // Note: commit counter before we invoke the tasks. // final long commitCounter = journal.getRootBlockView() // .getCommitCounter(); // // final WriteExecutorService writeService = journal // .getConcurrencyManager().getWriteService(); // // // Note: group commit counter before we invoke the tasks. // final long groupCommitCount0 = writeService.getGroupCommitCount(); // // // Note: #of failed tasks before we submit the tasks. // final long failedTaskCount0 = writeService.getTaskFailedCount(); // final long successTaskCount0 = writeService.getTaskSuccessCount(); // final long committedTaskCount0 = writeService.getTaskCommittedCount(); // // // Note: set by one of the tasks below. // final AtomicLong checkpointAddr2 = new AtomicLong(0L); // // final AtomicReference<Future<? extends Object>> futureB = new AtomicReference<Future<? extends Object>>(); // final AtomicReference<Future<? extends Object>> futureC = new AtomicReference<Future<? extends Object>>(); // final AtomicReference<Future<? extends Object>> futureD = new AtomicReference<Future<? extends Object>>(); // // /* // * Note: the setup for this test is a PITA. In order to exert full // * control over the order in which the tasks begin to execute we // * need to have each task submit the next itself. This is because it // * is possible for any of these tasks to be the first one to grab // * the exclusive lock on the necessary resource [name]. We can't // * solve this problem by restricting the #of threads that can run // * the tasks since that limits the size of the commit group. So we // * are stuck imposing serial execution using the behavior of the // * tasks themselves. // * // * Create the task objects in the reverse order of their execution. // */ // // // task (d) verifies expected rollback checkpoint was restored. // final AbstractTask d = new AbstractTask(journal,ITx.UNISOLATED,name){ // protected String getTaskName() {return "d";} // protected Object doTask() throws Exception { // // commit counter unchanged. // assertEquals("commitCounter", commitCounter, getJournal() // .getRootBlockView().getCommitCounter()); // if(checkpointAddr2.get()==0L) { // fail("checkpointAddr2 was not set"); // } // // lookup index. // BTree ndx = (BTree)getIndex(name); // final long newCheckpointAddr =ndx.getCheckpoint().getCheckpointAddr(); // // verify checkpoint != last committed checkpoint. // assertNotSame(checkpointAddr0,newCheckpointAddr); // // verify checkpoint == last rollback checkpoint. // assertEquals(checkpointAddr2.get(),newCheckpointAddr); // return null; // } // }; // // /* // * task (c) notes the last checkpoint, writes on the index, and then // * fails. This is designed to trigger rollback of the index to the // * last checkpoint, which is the checkpoint that we note at the // * start of this task. // */ // final AbstractTask c = new AbstractTask(journal,ITx.UNISOLATED,name){ // protected String getTaskName() {return "c";} // protected Object doTask() throws Exception { // // commit counter unchanged. // assertEquals("commitCounter", commitCounter, getJournal() // .getRootBlockView().getCommitCounter()); // // lookup index. // BTree ndx = (BTree)getIndex(name); // // note the last checkpoint written. // final long newCheckpointAddr = ndx.getCheckpoint().getCheckpointAddr(); // assertNotSame(0L,newCheckpointAddr); // assertNotSame(checkpointAddr0,newCheckpointAddr); // // make note of the checkpoint before we force an abort. // assertTrue("checkpointAddr2 already set?",checkpointAddr2.compareAndSet(0L, newCheckpointAddr)); // // write another record on the index. // ndx.insert(new byte[]{3}, new byte[]{3}); // // run task (d) next. // assertTrue(futureD.compareAndSet(null,journal.submit(d))); // // force task to about with dirty index. // throw new ForcedAbortException(); // } // }; // // // task (b) writes another record on the index. // final AbstractTask b = new AbstractTask(journal,ITx.UNISOLATED,name){ // protected String getTaskName() {return "b";} // protected Object doTask() throws Exception { // // commit counter unchanged. // assertEquals("commitCounter", commitCounter, getJournal() // .getRootBlockView().getCommitCounter()); // // lookup index. // BTree ndx = (BTree)getIndex(name); // // verify checkpoint was updated. // assertNotSame(checkpointAddr0,ndx.getCheckpoint().getCheckpointAddr()); // // write another record on the index. // ndx.insert(new byte[]{2}, new byte[]{2}); // // run task (c) next. // assertTrue(futureC.compareAndSet(null,journal.submit(c))); // return null; // } // }; // // // task (a) writes on index. // final AbstractTask a = new AbstractTask(journal,ITx.UNISOLATED,name){ // protected String getTaskName() {return "a";} // protected Object doTask() throws Exception { // // commit counter unchanged. // assertEquals("commitCounter", commitCounter, getJournal() // .getRootBlockView().getCommitCounter()); // // group commit counter unchanged. // assertEquals("groupCommitCounter", groupCommitCount0, // writeService.getGroupCommitCount()); // // lookup index. // BTree ndx = (BTree)getIndex(name); // // verify same checkpoint. // assertEquals(checkpointAddr0,ndx.getCheckpoint().getCheckpointAddr()); // // write record on the index. // ndx.insert(new byte[]{1}, new byte[]{1}); // // run task (b) next. // assertTrue(futureB.compareAndSet(null,journal.submit(b))); // return null; // } // }; // //// final List<AbstractTask> tasks = Arrays.asList(new AbstractTask[] { //// a,b,c,d //// }); //// //// final List<Future<Object>> futures = journal.invokeAll( tasks ); // // final Future<? extends Object> futureA = journal.submit( a ); // // /* // * wait for (a). if all tasks are in the same commit group then all // * tasks will be done once we have the future for (a). // */ // futureA.get(); // task (a) // // /* // * The expectation is that the tasks that succeed make it into the // * same commit group while the task that throws an exception does // * not cause the commit group to be aborted. Therefore there should // * be ONE (1) commit more than when we submitted the tasks. // * // * Note: The tasks will make it into the same commit group iff the // * first task that completes is willing to wait for the others to // * join the commit group. // * // * Note: The tasks have a dependency on the same resource so they // * will be serialized (executed in a strict sequence). // */ // assertEquals("failedTaskCount", failedTaskCount0 + 1, // writeService.getTaskFailedCount()); // assertEquals("successTaskCount", successTaskCount0 + 3, // writeService.getTaskSuccessCount()); // assertEquals("committedTaskCount", committedTaskCount0 + 3, // writeService.getTaskCommittedCount()); // assertEquals("groupCommitCount", groupCommitCount0 + 1, // writeService.getGroupCommitCount()); // assertEquals("commitCounter", commitCounter + 1, journal // .getRootBlockView().getCommitCounter()); // //// assertEquals( 4, futures.size()); // // futureB.get().get(); // task (b) // { // // task (c) did the abort. // Future f = futureC.get(); // try {f.get(); fail("Expecting exception");} // catch(ExecutionException ex) { // if(!InnerCause.isInnerCause(ex, ForcedAbortException.class)) { // fail("Expecting "+ForcedAbortException.class+", not "+ex, ex); // } // } // } // futureD.get().get(); // task (d) // // } finally { // // journal.destroy(); // // } // // } /** * A class used to force aborts on tasks and then recognize the abort by the * {@link ForcedAbortException} from the uni * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> */ @SuppressWarnings("unused") private class ForcedAbortException extends RuntimeException { private static final long serialVersionUID = 1L; } // /** // * Correctness test of task retry when another task causes a commit group to // * be discarded. // * // * @todo implement retry and maxLatencyFromSubmit and move the tests for // * those features into their own test suites. // * // * @todo Test retry of tasks (read only or read write) when they are part of // * a commit group in which some other task fails so they get // * interrupted and have to abort. // * // * @todo also test specifically with isolated tasks to make sure that the // * isolated indices are being rolled back to the last commit when the // * task is aborted. // */ // public void test_retry_readService() { // // fail("write this test"); // // } // // public void test_retry_writeService() { // // fail("write this test"); // // } // // public void test_retry_txService_readOnly() { // // fail("write this test"); // // } // // /* // * note: the difference between readOnly and readCommitted is that the // * latter MUST read from whatever the committed state of the index is at the // * time that it executes (or re-retries?) while the formed always reads from // * the state of the index as of the transaction start time. // */ // public void test_retry_txService_readCommitted() { // // fail("write this test"); // // } // // public void test_retry_txService_readWrite() { // // fail("write this test"); // // } // Note: I can not think of any way to write this test. // // /** // * This test verifies that unisolated reads are against the last committed // * state of the index(s), that they do NOT permit writes, and that // * concurrent writers on the same named index(s) do NOT conflict. // */ // public void test_submit_readService_isolation01() throws InterruptedException, ExecutionException { // // Properties properties = getProperties(); // // Journal journal = new Journal(properties); // // final long commitCounterBefore = journal.getRootBlockView().getCommitCounter(); // // final String resource = "foo"; // // final AtomicBoolean ran = new AtomicBoolean(false); // // final UUID indexUUID = UUID.randomUUID(); // // // create the index (and commit). // assertEquals("indexUUID", indexUUID, journal.submit( // new RegisterIndexTask(journal, resource, new UnisolatedBTree( // journal, indexUUID))).get()); // // // verify commit. // assertEquals("commit counter unchanged?", // commitCounterBefore+1, journal.getRootBlockView() // .getCommitCounter()); // // // write some data on the index (and commit). // final long metadataAddr = (Long) journal.submit( // new AbstractIndexTask(journal, ITx.UNISOLATED, // false/*readOnly*/, resource) { // // protected Object doTask() throws Exception { // // IIndex ndx = getIndex(getOnlyResource()); // // // Note: the metadata address before any writes on the index. // final long metadataAddr = ((BTree)ndx).getMetadata().getMetadataAddr(); // // // write on the index. // ndx.insert(new byte[]{1,2,3},new byte[]{2,2,3}); // // return metadataAddr; // // } // // }).get(); // // // verify another commit. // assertEquals("commit counter unchanged?", // commitCounterBefore+2, journal.getRootBlockView() // .getCommitCounter()); // // Future<Object> future = journal.submit(new AbstractIndexTask(journal, // ITx.UNISOLATED, true/*readOnly*/, resource) { // // /** // * The task just sets a boolean value and returns the name of the // * sole resource. It does not actually read anything. // */ // protected Object doTask() throws Exception { // // ran.compareAndSet(false, true); // // return getOnlyResource(); // // } // }); // // // the test task returns the resource as its value. // assertEquals("result",resource,future.get()); // // /* // * make sure that the flag was set (not reliably set until we get() the // * future). // */ // assertTrue("ran",ran.get()); // // /* // * Verify that a commit was NOT performed. // */ // assertEquals("commit counter changed?", // commitCounterBefore, journal.getRootBlockView() // .getCommitCounter()); // // journal.shutdown(); // // journal.delete(); // // } /** * A stress test that runs concurrent {@link ITx#READ_COMMITTED} readers and * {@link ITx#UNISOLATED} writers and verifies that readers are able to * transparently continue to read against the named indices if the backing * {@link FileChannel} is closed by an interrupt noticed during an IO on * that {@link FileChannel}. In order for this test to succeed the backing * {@link FileChannel} must be transparently re-opened in a thread-safe * manner if it is closed asynchronously (i.e., closed while the buffer * strategy is still open). * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a> */ public void test_concurrentReadersAreOk() throws Throwable { // Note: clone so that we do not modify!!! final Properties properties = new Properties(getProperties()); // Mostly serialize the readers. properties.setProperty(Options.READ_SERVICE_CORE_POOL_SIZE, "2"); // Completely serialize the writers. properties.setProperty(Options.WRITE_SERVICE_CORE_POOL_SIZE, "1"); properties.setProperty(Options.WRITE_SERVICE_MAXIMUM_POOL_SIZE, "1"); final Journal journal = new Journal(properties); // final IBufferStrategy bufferStrategy = journal.getBufferStrategy(); // if (bufferStrategy instanceof RWStrategy) { // ((RWStrategy)bufferStrategy).getRWStore().activateTx(); // } try { // Note: This test requires a journal backed by stable storage. if (journal.isStable()) { // register and populate the indices and commit. final int NRESOURCES = 10; final int NWRITES = 10000; final String[] resource = new String[NRESOURCES]; { final KeyBuilder keyBuilder = new KeyBuilder(4); for (int i = 0; i < resource.length; i++) { resource[i] = "index#" + i; final IIndex ndx = (IIndex) journal.register( resource[i], new IndexMetadata(resource[i], UUID.randomUUID())); for (int j = 0; j < NWRITES; j++) { final byte[] val = (resource[i] + "#" + j).getBytes(); ndx.insert(keyBuilder.reset().append(j).getKey(), val); } } journal.commit(); } log.warn("Registered and populated " + resource.length + " named indices with " + NWRITES + " records each"); /** * Does an {@link ITx#READ_COMMITTED} index scan on a named index * using the last committed state of the named index. * <p> * Note: The expectation is that the read tasks WILL NOT throw * exceptions related to the asynchronous close of the * {@link FileChannel} in response to a writer interrupted during a * FileChannel IO since we are expecting transparent re-opening of * the store. In practice this means that the buffer strategy * implementation must notice when the store was closed * asynchronously, obtain a lock, re-open the store, and then re-try * the operation. * * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan * Thompson</a> */ class ReadTask extends AbstractTask<Void> { protected ReadTask(IConcurrencyManager concurrencyManager, String resource) { super(concurrencyManager, ITx.READ_COMMITTED, resource); } @Override protected Void doTask() throws Exception { final IIndex ndx = getIndex(getOnlyResource()); // verify writes not allowed. try { ndx.insert(new byte[]{}, new byte[]{}); fail("Expecting: "+UnsupportedOperationException.class); }catch(UnsupportedOperationException ex) { log.info("Ingoring expected exception: "+ex); } // assertTrue(ndx instanceof ReadOnlyIndex); final ITupleIterator<?> itr = ndx.rangeIterator(null, null); int n = 0; while (itr.hasNext()) { itr.next(); n++; } assertEquals("#entries", n, NWRITES); return null; } } /* * Runs a sequence of write operations that interrupt themselves. */ final ExecutorService writerService = Executors .newSingleThreadExecutor(new DaemonThreadFactory()); /* * Submit tasks to the single threaded service that will in turn * feed them on by one to the journal's writeService. When run on * the journal's writeService the tasks will interrupt themselves * and, depending on whether or not the interrupt occurs during a * FileChannel IO, provoke the JDK to close the FileChannel backing * the journal. */ for (int i = 0; i < 10; i++) { final String theResource = resource[i % resource.length]; writerService.submit(new Callable<Object>() { @Override public Object call() throws Exception { journal.submit(new InterruptMyselfTask(journal, ITx.UNISOLATED, theResource)); // pause between submits Thread.sleep(20); return null; } }); } /* * Submit concurrent reader tasks and wait for them to run for a * while. */ { final Collection<AbstractTask<Void>> tasks = new LinkedList<AbstractTask<Void>>(); for (int i = 0; i < NRESOURCES * 10; i++) { tasks.add(new ReadTask(journal, resource[i % resource.length])); } // await futures. final List<Future<Void>> futures = journal.invokeAll(tasks, 10, TimeUnit.SECONDS); for(Future<Void> f : futures) { if(f.isDone()&&!f.isCancelled()) { // all tasks that complete should have done so without error. f.get(); } } } writerService.shutdownNow(); log.warn("End of test"); } } finally { // if (bufferStrategy instanceof RWStrategy) { // ((RWStrategy)bufferStrategy).getRWStore().deactivateTx(); // } journal.destroy(); } } }