/**
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 16, 2006
*/
package com.bigdata.journal;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.bigdata.btree.BTree;
import com.bigdata.btree.IIndex;
import com.bigdata.btree.ILocalBTreeView;
import com.bigdata.btree.IRangeQuery;
import com.bigdata.btree.ITuple;
import com.bigdata.btree.ITupleIterator;
import com.bigdata.btree.IndexMetadata;
import com.bigdata.btree.Tuple;
import com.bigdata.btree.isolation.IsolatedFusedView;
import com.bigdata.util.InnerCause;
/**
* Test suite for fully-isolated read-write transactions.
*
* @todo verify with writes on multiple indices (partial ordering over the
* commits)
*
* @todo verify partial ordering imposed on concurrent operations on the same tx
* for indices declared by those operations, etc.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @version $Id$
*/
public class TestTx extends ProxyTestCase<Journal> {
public TestTx() {
}
public TestTx(String name) {
super(name);
}
// /**
// * Writes some interesting constants on {@link System#err}.
// */
// public void test_constants() {
//
// if(log.isInfoEnabled()) log.info("min : "+new Date(Long.MIN_VALUE));
// if(log.isInfoEnabled()) log.info("min1: "+new Date(Long.MIN_VALUE+1));
// if(log.isInfoEnabled()) log.info("-1L : "+new Date(-1));
// if(log.isInfoEnabled()) log.info(" 0L : "+new Date(0L));
// if(log.isInfoEnabled()) log.info("max1: "+new Date(Long.MAX_VALUE-1));
// if(log.isInfoEnabled()) log.info("max : "+new Date(Long.MAX_VALUE));
//
// }
/**
* Test verifies that a transaction may start when there are (a) no commits
* on the journal; and (b) no indices have been registered.
* <P>
* Note: The transaction will be unable to isolate an index if the index has
* not been registered already by an unisolated operation.
*/
public void test_noIndicesRegistered() {
final Journal journal = getStore();
try {
journal.commit();
final long tx = journal.newTx(ITx.UNISOLATED);
/*
* nothing written on this transaction.
*/
// commit.
assertEquals(0L, journal.commit(tx));
} finally {
journal.destroy();
}
}
/**
* Verify that an index is not visible in the tx unless the native
* transaction in which it is registered has already committed before the tx
* starts.
*/
public void test_indexNotVisibleUnlessCommitted() {
final Journal journal = getStore();
try {
final String name = "abc";
// register index in unisolated scope, but do not commit yet.
{
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
}
// start tx1.
final long tx1 = journal.newTx(ITx.UNISOLATED);
// the index is not visible in tx1.
assertNull(journal.getIndex(name, tx1));
// do unisolated commit.
assertNotSame(0L, journal.commit());
// start tx2.
final long tx2 = journal.newTx(ITx.UNISOLATED);
// the index still is not visible in tx1.
assertNull(journal.getIndex(name, tx1));
// the index is visible in tx2.
assertNotNull(journal.getIndex(name, tx2));
journal.abort(tx1);
journal.abort(tx2);
} finally {
journal.destroy();
}
}
/**
* Test verifies that you always get the same object back when you ask for
* an isolated named index. This is important both to conserve resources and
* since the write set is in the isolated index -- you lose it and it is
* gone.
*/
public void test_sameIndexObject() {
final Journal journal = getStore();
try {
final String name = "abc";
{
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
journal.commit();
}
final long tx1 = journal.newTx(ITx.UNISOLATED);
final IIndex ndx1 = journal.getIndex(name, tx1);
assertNotNull(ndx1);
final long tx2 = journal.newTx(ITx.UNISOLATED);
final IIndex ndx2 = journal.getIndex(name, tx2);
assertTrue(tx1 != tx2);
assertTrue(ndx1 != ndx2);
assertNotNull(ndx2);
assertTrue(ndx1 == journal.getIndex(name, tx1));
assertTrue(ndx2 == journal.getIndex(name, tx2));
} finally {
journal.destroy();
}
}
/**
* Create a journal, setup an index, write an entry on that index, and
* commit the store. Setup a transaction and verify that we can isolate that
* index and read the written value. Write a value on the unisolated index
* and verify that it is not visible within the transaction.
*/
public void test_readIsolation() {
final Journal journal = getStore();
try {
final String name = "abc";
final byte[] k1 = new byte[] { 1 };
final byte[] k2 = new byte[] { 2 };
final byte[] v1 = new byte[] { 1 };
final byte[] v2 = new byte[] { 2 };
{
/*
* register the index, write an entry on the unisolated index,
* and commit the journal.
*/
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
IIndex index = journal.getIndex(name);
assertNull(index.insert(k1, v1));
assertNotSame(0L, journal.commit());
}
final long tx1 = journal.newTx(ITx.UNISOLATED);
if(log.isDebugEnabled())
log.debug("State A, tx1: " + tx1 + "\n" + showCRI(journal));
{
/*
* verify that the write is visible in a transaction that starts
* after the commit.
*/
IIndex index = journal.getIndex(name, tx1);
assertTrue(index.contains(k1));
assertEquals(v1, (byte[]) index.lookup(k1));
}
{
/*
* obtain the unisolated index and write another entry and
* commit the journal.
*/
IIndex index = journal.getIndex(name);
assertNull(index.insert(k2, v2));
assertTrue(index.contains(k2));
final long c2 = journal.commit();
if(log.isDebugEnabled())
log.debug("State B, c2: " + c2 + "\n" + showCRI(journal));
assertNotSame(0L, c2);
}
{
/*
* verify that the entry written on the unisolated index is not
* visible to the transaction that started before that write.
*/
IIndex index = journal.getIndex(name, tx1);
assertTrue(index.contains(k1));
assertFalse(index.contains(k2));
}
{
final IIndex index = journal.getIndex(name);
assertTrue(index.contains(k1));
assertTrue(index.contains(k2));
}
/*
* start another transaction and verify that the 2nd committed
* write is now visible to that transaction.
*/
final long tx2 = journal.newTx(ITx.UNISOLATED);
if(log.isDebugEnabled())
log.debug("tx1: " + tx1 + ", tx2: " + tx2 + "\n" + showCRI(journal));
{
/*
* start another transaction and verify that the 2nd committed
* write is now visible to that transaction.
*/
IIndex index = journal.getIndex(name, tx2);
assertTrue(index.contains(k1));
assertTrue(index.contains(k2));
}
journal.abort(tx1);
journal.abort(tx2);
} finally {
journal.destroy();
}
}
private static String showCRI(final Journal journal) {
final ITupleIterator<CommitRecordIndex.Entry> commitRecords;
/*
* Commit can be called prior to Journal initialisation, in which case
* the commitRecordIndex will not be set.
*/
final IIndex commitRecordIndex = journal.getReadOnlyCommitRecordIndex();
if (commitRecordIndex == null) { // TODO Why is this here?
return "EMPTY";
}
// final IndexMetadata metadata = commitRecordIndex.getIndexMetadata();
commitRecords = commitRecordIndex.rangeIterator();
StringBuilder out = new StringBuilder();
while (commitRecords.hasNext()) {
final ITuple<CommitRecordIndex.Entry> tuple = commitRecords.next();
final CommitRecordIndex.Entry entry = tuple.getObject();
try {
final ICommitRecord record = CommitRecordSerializer.INSTANCE
.deserialize(journal.read(entry.addr));
out.append(record.toString() + "\n");
} catch (RuntimeException re) {
throw new RuntimeException("Problem with entry at "
+ entry.addr, re);
}
}
return out.toString();
}
/**
* Test verifies that an isolated write is visible inside of a transaction
* (tx1) but not in a concurrent transaction (tx2) and not in the unisolated
* index until the tx1 commits. Once the tx1 commits, the write is visible
* in the unisolated index. The write never becomes visible in tx2. If tx2
* attempts to write a value under the same key then a write-write conflict
* is reported and validation fails.
*/
public void test_writeIsolation() {
final Journal journal = getStore();
try {
final String name = "abc";
final byte[] k1 = new byte[] { 1 };
final byte[] v1 = new byte[] { 1 };
final byte[] v1a = new byte[] { 1, 1 };
{
/*
* register an index and commit the journal.
*/
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
assertNotSame(0L, journal.commit());
}
/*
* create two transactions.
*/
final long tx1 = journal.newTx(ITx.UNISOLATED);
final long tx2 = journal.newTx(ITx.UNISOLATED);
assertNotSame(tx1, tx2);
assertTrue(Math.abs(tx1) >= journal.getRootBlockView()
.getLastCommitTime());
assertTrue(Math.abs(tx2) > Math.abs(tx1));
{
/*
* Write an entry in tx1.
*
* Verify that the entry is not visible in the unisolated index
* or in the index as isolated by tx2.
*/
final IsolatedFusedView ndx1 = (IsolatedFusedView) journal
.getIndex(name, tx1);
assertFalse(ndx1.contains(k1));
assertNull(ndx1.insert(k1, v1));
// existence check in tx1.
assertTrue(ndx1.contains(k1));
// not visible in the other tx.
assertFalse(journal.getIndex(name, tx2).contains(k1));
// not visible in the unisolated index.
assertFalse(journal.getIndex(name).contains(k1));
/*
* Commit tx1.
*
* Verify that the write is still not visible in tx2 but that it
* is now visible in the unisolated index.
*/
// grab hard ref. to the local state for the tx before commit()
final Tx localState = journal.getLocalTransactionManager()
.getTx(tx1);
// commit tx1.
final long commitTime1 = journal.commit(tx1);
assertNotSame(0L, commitTime1);
// still not visible in the other tx.
assertFalse(journal.getIndex(name, tx2).contains(k1));
// but now visible in the unisolated index.
assertTrue(journal.getIndex(name).contains(k1));
// check the version timestamp in the unisolated index.
{
final BTree btree = ((BTree) journal.getIndex(name));
final ITuple<?> tuple = btree.lookup(k1, new Tuple(btree,
IRangeQuery.ALL));
assertNotNull(tuple);
assertFalse(tuple.isDeletedVersion());
/*
* Verify that the revisionTime was written onto the tuple
* in the post-commit view of the unisolated index.
*/
assertEquals("revisionTime", localState.getRevisionTime(),
tuple.getVersionTimestamp());
}
/*
* write a conflicting entry in tx2 and verify that validation
* of tx2 fails.
*/
assertNull(journal.getIndex(name, tx2).insert(k1, v1a));
// check the version counter in tx2.
{
final IsolatedFusedView isolatedView = (IsolatedFusedView) journal
.getIndex(name, tx2);
final BTree btree = ((BTree) journal.getIndex(name));
Tuple<?> tuple = btree.lookup(k1, new Tuple(btree,
IRangeQuery.ALL));
tuple = isolatedView.getWriteSet().lookup(k1, tuple);
assertNotNull(tuple);
assertFalse(tuple.isDeletedVersion());
/*
* Verify the versionTimestamp on the tuple in the view
* isolated by [tx2]. It should now be the start time for
* tx2 since we just overwrote that tuple.
*/
assertEquals("versionTimestamp", Math.abs(tx2), tuple
.getVersionTimestamp());
}
try {
journal.commit(tx2);
fail("Expecting: " + ValidationError.class);
} catch (ValidationError ex) {
if(log.isInfoEnabled()) log.info("Ignoring expected exception: " + ex);
}
}
} finally {
journal.destroy();
}
}
//
// Delete object.
//
/**
* Two transactions (tx0, tx1) are created. A version (v0) is written onto
* tx0 for a key (id0). The test verifies the write and verifies that the
* write is not visible in tx1. The v0 is then deleted from tx0 and then
* another version (v1) is written on tx0 under the same key. Both
* transactions prepare and commit. The end state is that (id0,v1) is
* visible in the database after the commit.
*/
public void test_delete001() {
final Journal journal = getStore();
try {
final String name = "abc";
{
/*
* register an index and commit the journal.
*/
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
journal.commit();
}
/*
* create transactions.
*/
final long tx0 = journal.newTx(ITx.UNISOLATED);
final long tx1 = journal.newTx(ITx.UNISOLATED);
assertNotSame(tx0, tx1);
assertTrue(Math.abs(tx0) >= journal.getRootBlockView().getLastCommitTime());
assertTrue(Math.abs(tx1) > Math.abs(tx0));
/*
* Write v0 on tx0.
*/
final byte[] id0 = new byte[] { 0 };
final byte[] v0 = getRandomData().array();
journal.getIndex(name, tx0).insert(id0, v0);
assertEquals(v0, journal.getIndex(name, tx0).lookup(id0));
/*
* Verify that the version does NOT show up in a concurrent
* transaction.
*/
assertFalse(journal.getIndex(name, tx1).contains(id0));
// delete the version.
assertEquals(v0, journal.getIndex(name, tx0).remove(id0));
// no longer visible in that transaction.
assertFalse(journal.getIndex(name, tx0).contains(id0));
/*
* Test delete after delete (succeeds, but returns null).
*/
assertNull(journal.getIndex(name, tx0).remove(id0));
/*
* Test write after delete (succeeds, returning null).
*/
final byte[] v1 = getRandomData().array();
assertNull(journal.getIndex(name, tx0).insert(id0, v1));
// Still not visible in concurrent transaction.
assertFalse(journal.getIndex(name, tx1).contains(id0));
// Still not visible in global scope.
assertFalse(journal.getIndex(name).contains(id0));
// Prepare and commit tx0.
assertNotSame(0L, journal.commit(tx0));
// Still not visible in concurrent transaction.
assertFalse(journal.getIndex(name, tx1).contains(id0));
// Now visible in global scope.
assertTrue(journal.getIndex(name).contains(id0));
// Prepare and commit tx1 (no writes).
assertEquals(0L, journal.commit(tx1));
// Still visible in global scope.
assertTrue(journal.getIndex(name).contains(id0));
} finally {
journal.destroy();
}
}
/**
* Two transactions (tx0, tx1) are created. A version (v0) is written onto
* tx0 for a key (id0). The test verifies the write and verifies that the
* write is not visible in tx1. The v0 is then deleted from tx0 and then
* another version (v1) is written on tx0 under the same key and isolation
* is re-verified. Finally, v1 is deleted from tx0. Both transactions
* prepare and commit. The end state is that no entry for id0 is visible in
* the database after the commit.
*/
public void test_delete002() {
final Journal journal = getStore();
try {
final String name = "abc";
{
/*
* register an index and commit the journal.
*/
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
journal.commit();
}
/*
* create transactions.
*/
final long tx0 = journal.newTx(ITx.UNISOLATED);
final long tx1 = journal.newTx(ITx.UNISOLATED);
assertNotSame(tx0, tx1);
assertTrue(Math.abs(tx0) >= journal.getRootBlockView().getLastCommitTime());
assertTrue(Math.abs(tx1) > Math.abs(tx0));
/*
* Write v0 on tx0.
*/
final byte[] id0 = new byte[] { 1 };
final byte[] v0 = getRandomData().array();
journal.getIndex(name, tx0).insert(id0, v0);
assertEquals(v0, journal.getIndex(name, tx0).lookup(id0));
/*
* Verify that the version does NOT show up in a concurrent
* transaction.
*/
assertFalse(journal.getIndex(name, tx1).contains(id0));
// delete the version.
assertEquals(v0, (byte[]) journal.getIndex(name, tx0).remove(id0));
// no longer visible in that transaction.
assertFalse(journal.getIndex(name, tx0).contains(id0));
/*
* Test delete after delete (succeeds, but returns null).
*/
assertNull(journal.getIndex(name, tx0).remove(id0));
/*
* Test write after delete (succeeds, returning null).
*/
final byte[] v1 = getRandomData().array();
assertNull(journal.getIndex(name, tx0).insert(id0, v1));
// Still not visible in concurrent transaction.
assertFalse(journal.getIndex(name, tx1).contains(id0));
// Still not visible in global scope.
assertFalse(journal.getIndex(name).contains(id0));
/*
* Delete v1.
*/
assertEquals(v1, (byte[]) journal.getIndex(name, tx0).remove(id0));
// Still not visible in concurrent transaction.
assertFalse(journal.getIndex(name, tx1).contains(id0));
// Still not visible in global scope.
assertFalse(journal.getIndex(name).contains(id0));
/*
* Prepare and commit tx0.
*
* Note: We MUST NOT propagate a delete marker onto the unisolated
* index since no entry for that key is visible was visible when the
* tx0 began.
*
* Note: this should wind up as a NOP commit since the net result
* will be no writes on the unisolated index and hence no writes
* on the backing store.
*/
assertEquals(0L, journal.commit(tx0));
// Still not visible in concurrent transaction.
assertFalse(journal.getIndex(name, tx1).contains(id0));
// Still not visible in global scope.
assertFalse(journal.getIndex(name).contains(id0));
// Prepare and commit tx1 (no writes).
assertEquals(0L, journal.commit(tx1));
// Still not visible in global scope.
assertFalse(journal.getIndex(name).contains(id0));
} finally {
journal.destroy();
}
}
/**
* Two transactions (tx0, tx1) are created. A version (v0) is written onto
* tx0 for a key (id0). The test verifies the write and verifies that the
* write is not visible in tx1. The v0 is then deleted from tx0. Another
* version (v1) is written on tx1 under the same key and isolation is
* re-verified. Both transactions prepare and commit. Since no entry for id0
* was pre-existing in the global scope the delete in tx0 does not propagate
* into the global scope and the end state is that the write (id0,v1) from
* tx1 is visible in the database after the commit.
*/
public void test_delete003() {
final Journal journal = getStore();
try {
final String name = "abc";
{
/*
* register an index and commit the journal.
*/
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
journal.commit();
}
/*
* create transactions.
*/
final long tx0 = journal.newTx(ITx.UNISOLATED);
final long tx1 = journal.newTx(ITx.UNISOLATED);
assertNotSame(tx0, tx1);
assertTrue(Math.abs(tx0) >= journal.getRootBlockView()
.getLastCommitTime());
assertTrue(Math.abs(tx1) > Math.abs(tx0));
/*
* Write v0 on tx0.
*/
final byte[] id0 = new byte[] { 1 };
final byte[] v0 = getRandomData().array();
journal.getIndex(name, tx0).insert(id0, v0);
assertEquals(v0, journal.getIndex(name, tx0).lookup(id0));
/*
* Verify that the version does NOT show up in a concurrent
* transaction.
*/
assertFalse(journal.getIndex(name, tx1).contains(id0));
// delete the version.
assertEquals(v0, (byte[]) journal.getIndex(name, tx0).remove(id0));
// no longer visible in that transaction.
assertFalse(journal.getIndex(name, tx0).contains(id0));
/*
* write(id0,v1) in tx1.
*/
final byte[] v1 = getRandomData().array();
assertNull(journal.getIndex(name, tx1).insert(id0, v1));
// Still not visible in concurrent transaction.
assertFalse(journal.getIndex(name, tx0).contains(id0));
// Still not visible in global scope.
assertFalse(journal.getIndex(name).contains(id0));
/*
* Prepare and commit tx0.
*
* Note: We MUST NOT propagate a delete marker onto the unisolated
* index since no entry for that key is visible was visible when the
* tx0 began.
*
* Note: this should wind up as a NOP commit since the net result
* will be no writes on the unisolated index and hence no writes
* on the backing store.
*/
assertEquals(0L, journal.commit(tx0));
// Prepare and commit tx1.
assertNotSame(0L, journal.commit(tx1));
// (id0,v1) is now visible in global scope.
assertTrue(journal.getIndex(name).contains(id0));
assertEquals(v1, (byte[]) journal.getIndex(name).lookup(id0));
} finally {
journal.destroy();
}
}
/*
* Transaction semantics tests.
*/
/**
* Simple test of commit semantics (no conflict). Four transactions are
* started: tx0, which starts first. tx1 which starts next and on which we
* will write one data version; tx2, which begins after tx1 but before tx1
* commits - the change will NOT be visible in this transaction; and tx3,
* which begins after tx1 commits - the change will be visible in this
* transaction.
*/
public void test_commit_noConflict01() {
final Journal journal = getStore();
try {
final String name = "abc";
final long commitTime0;
{
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
commitTime0 = journal.commit();
if(log.isInfoEnabled()) log.info("commitTime0: " + journal.getCommitRecord());
assertNotSame(0L, commitTime0);
assertEquals("commitCounter", 1L, journal.getCommitRecord()
.getCommitCounter());
}
/*
* Transaction that starts before the transaction on which we write.
* The change will not be visible in this scope.
*/
final long tx0 = journal.newTx(ITx.UNISOLATED);
// transaction on which we write and later commit.
final long tx1 = journal.newTx(ITx.UNISOLATED);
// new transaction - commit will not be visible in this scope.
final long tx2 = journal.newTx(ITx.UNISOLATED);
if(log.isInfoEnabled()) log.info("commitTime0 =" + commitTime0);
if(log.isInfoEnabled()) log.info("tx0: startTime=" + tx0);
if(log.isInfoEnabled()) log.info("tx1: startTime=" + tx1);
if(log.isInfoEnabled()) log.info("tx2: startTime=" + tx2);
assertTrue(commitTime0 <= Math.abs(tx0));
assertTrue(Math.abs(tx0) < Math.abs(tx1));
assertTrue(Math.abs(tx1) < Math.abs(tx2));
final byte[] id1 = new byte[] { 1 };
final byte[] v0 = getRandomData().array();
// write data version on tx1
assertNull(journal.getIndex(name, tx1).insert(id1, v0));
// data version visible in tx1.
assertEquals(v0, (byte[]) journal.getIndex(name, tx1).lookup(id1));
// data version not visible in global scope.
assertNull(journal.getIndex(name).lookup(id1));
// data version not visible in tx0.
assertNull(journal.getIndex(name, tx0).lookup(id1));
// data version not visible in tx2.
assertNull(journal.getIndex(name, tx2).lookup(id1));
// commit.
final long tx1CommitTime = journal.commit(tx1);
assertNotSame(0L, tx1CommitTime);
if(log.isInfoEnabled()) log.info("tx1: startTime=" + tx1 + ", commitTime="
+ tx1CommitTime);
if(log.isInfoEnabled()) log.info("tx1: after commit: "
+ journal.getCommitRecord());
assertEquals("commitCounter", 2L, journal.getCommitRecord()
.getCommitCounter());
// data version now visible in global scope.
assertEquals(v0, (byte[]) journal.getIndex(name).lookup(id1));
// new transaction - commit is visible in this scope.
final long tx3 = journal.newTx(ITx.UNISOLATED);
assertTrue(Math.abs(tx2) < Math.abs(tx3));
assertTrue(Math.abs(tx3) >= tx1CommitTime);
if(log.isInfoEnabled()) log.info("tx3: startTime=" + tx3);
// if(log.isInfoEnabled()) log.info("tx3: ground state:
// "+((Tx)journal.getTx(tx3)).commitRecord);
// data version still not visible in tx0.
assertNull(journal.getIndex(name, tx0).lookup(id1));
// data version still not visible in tx2.
assertNull(journal.getIndex(name, tx2).lookup(id1));
/*
* What commit record was written by tx1 and what commit record is
* being used by tx3?
*/
// data version visible in the new tx (tx3).
assertEquals(v0, (byte[]) journal.getIndex(name, tx3).lookup(id1));
/*
* commit tx0 - nothing was written, no conflict should result.
*/
assertEquals(0L, journal.commit(tx0));
assertEquals("commitCounter", 2L, journal.getCommitRecord()
.getCommitCounter());
/*
* commit tx1 - nothing was written, no conflict should result.
*/
assertEquals(0L, journal.commit(tx2));
assertEquals("commitCounter", 2L, journal.getCommitRecord()
.getCommitCounter());
// commit tx3 - nothing was written, no conflict should result.
assertEquals(0L, journal.commit(tx3));
assertEquals("commitCounter", 2L, journal.getCommitRecord()
.getCommitCounter());
// data version in global scope was not changed by any other commit.
assertEquals(v0, (byte[]) journal.getIndex(name).lookup(id1));
} finally {
journal.destroy();
}
}
/**
* Test in which a transaction deletes a pre-existing version (that is, a
* version that existed in global scope when the transaction was started).
*/
public void test_deletePreExistingVersion_noConflict() {
final Journal journal = getStore();
try {
final String name = "abc";
{
IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
md.setIsolatable(true);
journal.registerIndex(md);
journal.commit();
}
final byte[] id0 = new byte[] { 1 };
final byte[] v0 = getRandomData().array();
// data version not visible in global scope.
assertNull(journal.getIndex(name).lookup(id0));
// write data version in global scope.
journal.getIndex(name).insert(id0, v0);
// data version visible in global scope.
assertEquals(v0, journal.getIndex(name).lookup(id0));
// commit the unisolated write.
journal.commit();
// start transaction.
final long tx0 = journal.newTx(ITx.UNISOLATED);
// data version visible in the transaction.
assertEquals(v0, journal.getIndex(name, tx0).lookup(id0));
// delete version in transaction scope.
assertEquals(v0, journal.getIndex(name, tx0).remove(id0));
// data version still visible in global scope.
assertTrue(journal.getIndex(name).contains(id0));
assertEquals(v0, journal.getIndex(name).lookup(id0));
// data version not visible in transaction.
assertFalse(journal.getIndex(name, tx0).contains(id0));
assertNull(journal.getIndex(name, tx0).lookup(id0));
// commit.
journal.commit(tx0);
// data version now deleted in global scope.
assertFalse(journal.getIndex(name).contains(id0));
} finally {
journal.destroy();
}
}
/**
* Unit test written to verify that a read-only tx gets the same view of an
* index when it has the same ground state as another read-only tx. That
* view should be a simple {@link BTree} rather than an
* {@link IsolatedFusedView}.
*
* @see <a href="https://sourceforge.net/apps/trac/bigdata/ticket/266">
* Refactor native long tx id to thin object</a>
*
* @see <a href="http://sourceforge.net/apps/trac/bigdata/ticket/546" > Add
* cache for access to historical index views on the Journal by name
* and commitTime. </a>
*/
public void test_readOnlyTx() {
final Journal journal = getStore();
try {
final String name = "abc";
final long commitTime1;
{
final IndexMetadata md = new IndexMetadata(name, UUID.randomUUID());
/*
* Note: Thie tests w/ and w/o isolation. Isolation is NOT
* required for this. We are testing the semantics of read-only
* transactions and those are available for indices which do NOT
* support isolation as well as those which do.
*/
md.setIsolatable(r.nextBoolean());
journal.registerIndex(md);
commitTime1 = journal.commit();
}
// The unisolated index view.
final BTree un = journal.getIndex(name);
// The index exists.
assertNotNull(un);
// The unisolated view is not in the historical index cache.
assertEquals("historicalIndexCacheSize", 0,
journal.getHistoricalIndexCacheSize());
// The unisolated view is not in the index cache.
assertEquals("indexCacheSize", 0, journal.getIndexCacheSize());
// start 2 transactions. they will read from the same commit point.
final long tx0 = journal.newTx(ITx.READ_COMMITTED);
final long tx1 = journal.newTx(ITx.READ_COMMITTED);
// txids MUST be distinct.
assertTrue(tx0 != tx1);
// resolve the same index for those transactions.
final ILocalBTreeView tx0Index = journal.getIndex(name, tx0);
final ILocalBTreeView tx1Index = journal.getIndex(name, tx1);
/*
* The index shows up exactly once in the historical index cache.
*
* Note: The Name2Addr index will also wind up in this cache when it
* is fetched for a historical commit time.
*/
assertEquals("historicalIndexCacheSize", 1 + 1 /* Name2Addr */,
journal.getHistoricalIndexCacheSize());
/*
* The index shows up exactly once in the index cache.
*
* Note: Name2Addr is NOT present in this cache.
*/
assertEquals("indexCacheSize", 1, journal.getIndexCacheSize());
// Verify that the views are BTree instances vs IsolatedFusedViews
if (!(tx0Index instanceof BTree))
fail("Expecting " + BTree.class + " but have "
+ tx0Index.getClass());
if (!(tx1Index instanceof BTree))
fail("Expecting " + BTree.class + " but have "
+ tx1Index.getClass());
// Lookup the underlying ITx objects.
final Tx tx0Obj = (Tx) journal.getTransactionManager().getTx(tx0);
final Tx tx1Obj = (Tx) journal.getTransactionManager().getTx(tx1);
// Should be the commitTime for the commit above.
assertEquals("commitTime", commitTime1,
tx0Obj.getReadsOnCommitTime());
// Should read on the same commit time.
assertEquals("readsOnCommitTime", tx0Obj.getReadsOnCommitTime(),
tx1Obj.getReadsOnCommitTime());
// The tranaction objects must be distinct.
assertTrue(tx0Obj != tx1Obj);
// The indices are the same reference.
assertTrue(tx0Index == tx1Index);
// The read-only views are not the same as the unisolated view.
assertFalse(un == tx0Index);
} finally {
journal.destroy();
}
}
/**
* Stress test for concurrent transactions against a single named index.
*/
public void testStress() throws InterruptedException, ExecutionException {
final int ntx = 30;
final int nops = 10000;
final Journal store = getStore();
try {
/*
* Register the index. Each store can hold multiple named indices.
*/
{
final IndexMetadata indexMetadata = new IndexMetadata(
"testIndex", UUID.randomUUID());
/*
* Note: You MUST explicitly enable transaction processing for a
* B+Tree when you register the index. Transaction processing
* requires that the index maintain both per-tuple delete
* markers and per-tuple version identifiers. While scale-out
* indices always always maintain per-tuple delete markers,
* neither local nor scale-out indices maintain the per-tuple
* version identifiers by default.
*/
indexMetadata.setIsolatable(true);
// register the index.
store.registerIndex(indexMetadata);
// commit the store so the index is on record.
store.commit();
}
/*
* Run a set of concurrent tasks. Each task executes within its own
* transaction. Conflicts between the transactions are increasingly
* likely as the #of transactions or the #of operations per
* transaction increases. When there is a conflict, the transaction
* for which the conflict was detected will be aborted when it tries
* to commit.
*/
final List<Callable<Void>> tasks = new LinkedList<Callable<Void>>();
for (int i = 0; i < ntx; i++) {
tasks.add(new ReadWriteTxTask(store, "testIndex", nops));
}
// run tasks on the journal's executor service.
final List<Future<Void>> futures = store.getExecutorService()
.invokeAll(tasks);
int i = 0;
int nok = 0;
for (Future<Void> future : futures) {
// check for errors.
try {
future.get();
nok++;
} catch (ExecutionException ex) {
if (InnerCause.isInnerCause(ex, ValidationError.class)) {
/*
* Normal exception. There was a conflict and one or the
* transactions could not be committed.
*/
System.out
.println("Note: task["
+ i
+ "] could not be committed due to a write-write conflict.");
} else {
// Unexpected exception.
throw ex;
}
}
i++;
}
/*
* Show #of transactions which committed successfully and the #of
* transactions which were executed.
*/
System.out.println("" + nok + " out of " + tasks.size()
+ " transactions were committed successfully.");
/*
* Show the operations executed and #of tuples in the B+Tree.
*/
System.out.println("nops=" + (nops * tasks.size())
+ ", rangeCount="
+ store.getIndex("testIndex").rangeCount());
System.out.println(new Date().toString());
} finally {
// destroy the backing store.
store.destroy();
}
}
/**
* Task performs random CRUD operations, range counts, and range scans
* isolated by a transaction and commits when it is done.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan
* Thompson</a>
* @version $Id$
*/
static class ReadWriteTxTask implements Callable<Void> {
final Journal jnl;
final String indexName;
final int nops;
final Random r = new Random();
// #of distinct keys for this task.
final int range = 1000;
/**
*
* @param jnl
* The journal.
* @param indexName
* The name of the index.
* @param nops
* The #of operations to execute against that {@link BTree}.
*/
public ReadWriteTxTask(final Journal jnl, final String indexName,
final int nops) {
this.jnl = jnl;
this.indexName = indexName;
this.nops = nops;
}
/**
* Starts a read-write transaction, obtains a view of the B+Tree
* isolated by the transaction, performs a series of operations on the
* isolated view, and then commits the transaction.
* <p>
* Note: When multiple instances of this task are run concurrently it
* becomes increasingly likely that a write-write conflict will be
* detected when you attempt to commit the transaction, in which case
* the commit(txid) will fail and an appropriate error will be thrown
* out of the task.
*
* @throws ValidationError
* if there is a write-write conflict during commit
* processing (the transaction write set conflicts with the
* write set of a concurrent transaction which has already
* successfully committed).
* @throws Exception
*/
public Void call() throws Exception {
// Start a transaction.
final long txid = jnl.newTx(ITx.UNISOLATED);
try {
/*
* Obtain a view of the index isolated by the transaction.
*/
final IIndex ndx = jnl.getIndex(indexName, txid);
for (int i = 0; i < nops; i++) {
switch (r.nextInt(4)) {
case 0:
/*
* write on the index, inserting or updating the value
* for the key.
*/
ndx.insert("key#" + r.nextInt(range), r.nextLong());
break;
case 1:
/* write on the index, removing the key iff found. */
ndx.remove("key#" + r.nextInt(range));
break;
case 2:
/*
* lookup a key in the index.
*/
ndx.lookup("key#" + r.nextInt(range));
break;
case 3:
/*
* range count the index.
*/
ndx.rangeCount();
break;
case 4: {
/*
* run a range iterator over the index.
*/
final Iterator<ITuple<?>> itr = ndx.rangeIterator();
while (itr.hasNext()) {
itr.next();
}
break;
}
default:
throw new AssertionError("case not handled");
}
}
} catch (Throwable t) {
jnl.abort(txid);
throw new RuntimeException(t);
}
/*
* Commit the transaction. if the commit fails, then the transaction
* is aborted.
*/
jnl.commit(txid);
// done.
return null;
}
}
}