/**
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 Mar 14, 2006
*
* $Id$
*/
package com.bigdata.concurrent;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.Vector;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.apache.log4j.Logger;
import com.bigdata.concurrent.TxDag.Edge;
/**
* Test suite for online transaction deadlock algorithm.
*
* @version $Id$
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
*
* @todo Write tests for the batch oriented methods
* {@link TxDag#addEdges(Object, Object[])} and
* {@link TxDag#removeEdges(Object, boolean)}. In particular, addEdges()
* must be atomic and removeEdges() must be tested both when the
* transactions is and is not known to be waiting on one or more other
* transactions. Also verify that getOrder() and resetOrder() are invoked
* properly.
*
* FIXME We need to verify that the implementation of the update closure
* algorithm is correct since it has significant complexity in order to minimize
* its computational cost. One option is to make sure that the optimized
* algorithm has the same behavior as the algorithm without any optimizations by
* comparing expected and actual path count matrices. Another option is to write
* a brute force algorithm to compute the closure of W without regard to the
* path count matrix and verify that the predications made that algorithm are
* indeed observed.
*
* @todo In order to verify that the implemention is correct we also need to
* verify whether or not a deadlock is correctly reported after each
* insert (removal never results in a deadlock). The easiest way to do
* this is to implement an alternative algorithm to search for cycles in
* W. Such an algorithm will be more expensive to run, but will use a
* different logic and can therefore be used to verify that deadlocks are
* being correctly predicted by the {@link TxDag} implementation.
*
* @todo Write tests of getEdges() when the closure is requested.
*
* @todo Write a pure performance test either at this level or in terms of the
* 2PL logic that will interact with the DAG. The
* {@link #testSymmetricOperations()} test is not appropriate since it is
* making copies of large amounts of data in order to verify that the
* operations are correctly reversed. Run the performance test at various
* concurrency levels so that we can characterize the actual cost of the
* implementation as a function of the concurrency. Note that while
* concurrency drives the size of the matrices, only the "in-use" indices
* from the matrices are actually visited when the closure of W* is
* updated.
*/
public class TestTxDag extends TestCase
{
public static final Logger log = Logger.getLogger
( TestTxDag.class
);
public TestTxDag()
{
}
public TestTxDag( String name )
{
super( name );
}
/**
* Constructor tests.
*/
public void test_ctor()
{
try {
new TxDag(-1);
fail( "Expecting: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info( "Expected exception: "+ex);
}
try {
new TxDag(0);
fail( "Expecting: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info( "Expected exception: "+ex);
}
try {
new TxDag(1);
fail( "Expecting: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info( "Expected exception: "+ex);
}
new TxDag(2);
new TxDag(20);
new TxDag(2000);
}
/**
* Test ability to generate unique transaction identifier used by
* {@link TxDag}to index into its internal arrays. This test verifies that
* insert is conditional, that lookup fails if tx was not registered, and
* that we can insert and then lookup a transaction in the DAG. The test
* also verifies that the {@link TxDag#size()}updates as vertices are added
* to the graph and does not update when the vertex already exists in the
* graph.
*/
public void test_lookup_001()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
assertEquals( "capacity", CAPACITY, dag.capacity() );
assertEquals( "size", 0, dag.size() );
/*
* Lookup a vertex that does not exist and verify that the vertex was
* not added.
*/
assertEquals( TxDag.UNKNOWN, dag.lookup( "v1", false ) );
assertEquals( TxDag.UNKNOWN, dag.lookup( "v1", false ) );
assertEquals( "size", 0, dag.size() );
/*
* Insert a vertex.
*/
final int t1 = dag.lookup( "v1", true );
assertTrue(TxDag.UNKNOWN != t1 );
assertEquals( "size", 1, dag.size() );
/*
* Lookup that vertex again.
*/
assertEquals( t1, dag.lookup( "v1", false ) );
assertEquals( "size", 1, dag.size() );
log.info( dag.toString() );
}
/**
* Correct rejection test when transaction object is <code>null</code>.
*/
public void test_lookup_002()
{
try {
TxDag dag = new TxDag( 1 );
dag.lookup(null, false );
fail( "Expecting: "+IllegalArgumentException.class );
}
catch( IllegalArgumentException ex ) {
log.info( "Ignoring expected exception: "+ex);
}
}
/**
* Test capacity limits. This test verifies that insert fails if capacity
* the graph (the maximum #of vertices) would be exceeded.
*/
public void test_capacity_001()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
Object[] tx = new String[ CAPACITY ];
assertEquals( "capacity", 0, dag.size() );
assertEquals( "size", 0, dag.size() );
for( int i=0; i<CAPACITY; i++ ) {
tx[ i ] = ""+i;
dag.lookup( tx[i], true );
assertEquals( "capacity", CAPACITY, dag.capacity() );
assertEquals( "size", i+1, dag.size() );
}
assertEquals( "capacity", CAPACITY, dag.size() );
assertEquals( "size", CAPACITY, dag.size() );
try {
dag.lookup( ""+CAPACITY, true );
fail( "Expecting: "+IllegalStateException.class );
}
catch( IllegalStateException ex ) {
log.info( "Ignoring expected exception: "+ex);
}
}
/**
* Simple tests of {@link TxDag#addEdge(Object, Object)},
* {@link TxDag#hasEdge(Object, Object)}and friends.
*/
public void test_addEdge_001()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
Object tx1 = "tx1";
Object tx2 = "tx2";
Object tx3 = "tx3";
assertEquals("size",0,dag.size());
assertSameValuesAnyOrder(new int[]{}, dag.getOrder());
dag.addEdge(tx1,tx2); // tx1 -> tx2 (aka tx1 WAITS_FOR tx2)
assertEquals("size",2,dag.size());
assertTrue( "tx1->tx2", dag.hasEdge(tx1,tx2) );
assertFalse("tx2->tx1", dag.hasEdge(tx2, tx1));
assertSameValuesAnyOrder(new int[]{dag.lookup(tx1, false),dag.lookup(tx2,false)}, dag.getOrder());
assertSameEdges(new Edge[]{new Edge(tx1,tx2,true)}, dag.getEdges(false));
dag.addEdge(tx2,tx3);
assertEquals("size",3,dag.size());
assertTrue( "tx1->tx2", dag.hasEdge(tx1,tx2) );
assertTrue( "tx2->tx3", dag.hasEdge(tx2,tx3) );
assertSameValuesAnyOrder(new int[]{dag.lookup(tx1, false),dag.lookup(tx2,false),dag.lookup(tx3,false)}, dag.getOrder());
assertSameEdges(new Edge[]{new Edge(tx1,tx2,true),new Edge(tx2,tx3,true)}, dag.getEdges(false));
}
/**
* Test for correct rejection of addEdge() when edge already exists.
*/
public void test_addEdge_correctRejection_001()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
Object tx1 = "tx1";
Object tx2 = "tx2";
dag.addEdge(tx1,tx2); // tx1 -> tx2 (aka tx1 WAITS_FOR tx2)
try {
dag.addEdge(tx1,tx2);
fail( "Expecting exception: "+IllegalStateException.class);
}
catch( IllegalStateException ex ) {
log.info("Expected exception: "+ex);
}
}
/**
* Test for correct rejection of addEdge() when either parameter is
* null or when both parameters are the same.
*/
public void test_addEdge_correctRejection_002()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
Object tx1 = "tx1";
try {
dag.addEdge(tx1,tx1);
fail( "Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
try {
dag.addEdge(null,tx1);
fail( "Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
try {
dag.addEdge(tx1,null);
fail( "Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
}
/**
* Test for correct rejection of addEdges() when an edge already exists.
*/
public void test_addEdges_correctRejection_001()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
Object tx1 = "tx1";
Object tx2 = "tx2";
dag.addEdges(tx1,new Object[]{tx2}); // tx1 -> tx2 (aka tx1 WAITS_FOR tx2)
try {
dag.addEdges(tx1,new Object[]{tx2});
fail( "Expecting exception: "+IllegalStateException.class);
}
catch( IllegalStateException ex ) {
log.info("Expected exception: "+ex);
}
}
/**
* Test for correct rejection of addEdge() when either parameter is
* null, when one of the targets is null, or when one of the targets
* is given more than once.
*/
public void test_addEdges_correctRejection_002()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
Object tx1 = "tx1";
Object tx2 = "tx2";
try {
dag.addEdges(null,new Object[]{tx1});
fail( "Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
try {
dag.addEdges(tx1,null);
fail( "Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
try {
dag.addEdges(tx1,new Object[]{tx1,tx2,tx1});
fail( "Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
}
/**
* Verify that {@link TxDag#lookup(Object, boolean)} does not cause
* {@link TxDag#getOrder()} to include the new vertex until an edge has been
* asserted for that vertex.
* <p>
* This also tests {@link TxDag#getOrder(int t, int u)}, which is similar
* to {@link TxDag#getOrder()} but it always includes the specified vertices
* in the returned int[].
*
* @see TxDag#lookup(Object, boolean)
* @see TxDag#getOrder()
*/
public void test_getOrder_001()
{
final int CAPACITY = 5;
TxDag dag = new TxDag( CAPACITY );
assertSameValuesAnyOrder( new int[]{}, dag.getOrder() );
final String tx1 = "tx1";
final int tx1_id = dag.lookup(tx1,true);
assertSameValuesAnyOrder( new int[]{}, dag.getOrder() );
final String tx2 = "tx2";
final int tx2_id = dag.lookup(tx2,true);
assertSameValuesAnyOrder( new int[]{}, dag.getOrder() );
assertSameValuesAnyOrder( new int[]{tx1_id,tx2_id}, dag.getOrder(tx1_id,tx2_id) );
assertEquals("inbound(tx1)",0,dag.inbound[tx1_id]);
assertEquals("outbound(tx1)",0,dag.outbound[tx1_id]);
assertEquals("inbound(tx2)",0,dag.inbound[tx2_id]);
assertEquals("outbound(tx2)",0,dag.outbound[tx2_id]);
dag.addEdge(tx1, tx2);
assertSameValuesAnyOrder( new int[]{tx1_id,tx2_id}, dag.getOrder() );
assertSameValuesAnyOrder( new int[]{tx1_id,tx2_id}, dag.getOrder(tx1_id,tx2_id) );
assertEquals("inbound(tx1)",0,dag.inbound[tx1_id]);
assertEquals("outbound(tx1)",1,dag.outbound[tx1_id]);
assertEquals("inbound(tx2)",1,dag.inbound[tx2_id]);
assertEquals("outbound(tx2)",0,dag.outbound[tx2_id]);
dag.removeEdge(tx1, tx2);
assertSameValuesAnyOrder( new int[]{}, dag.getOrder() );
assertSameValuesAnyOrder( new int[]{tx1_id,tx2_id}, dag.getOrder(tx1_id,tx2_id) );
assertEquals("inbound(tx1)",0,dag.inbound[tx1_id]);
assertEquals("outbound(tx1)",0,dag.outbound[tx1_id]);
assertEquals("inbound(tx2)",0,dag.inbound[tx2_id]);
assertEquals("outbound(tx2)",0,dag.outbound[tx2_id]);
dag.addEdge(tx2, tx1);
assertSameValuesAnyOrder( new int[]{tx1_id,tx2_id}, dag.getOrder() );
assertSameValuesAnyOrder( new int[]{tx1_id,tx2_id}, dag.getOrder(tx1_id,tx2_id) );
assertEquals("inbound(tx1)",1,dag.inbound[tx1_id]);
assertEquals("outbound(tx1)",0,dag.outbound[tx1_id]);
assertEquals("inbound(tx2)",0,dag.inbound[tx2_id]);
assertEquals("outbound(tx2)",1,dag.outbound[tx2_id]);
}
/**
* Verifies that <i>actual </i> contains all of the same values as
* <i>expected </i> in the same order.
*
* @param expected
* An integer array.
*
* @param actual
* Another integer array.
*/
public void assertSameValues( int[] expected, int[] actual )
{
assertEquals("length",expected.length,actual.length);
final int len = expected.length;
for( int i=0; i<len; i++ ) {
assertEquals("position="+i, expected[i], actual[i] );
}
}
/**
* Verifies that <i>actual </i> contains all of the same values as
* <i>expected </i> without regard to order.
*
* @param expected
* An integer array.
*
* @param actual
* Another integer array.
*/
public void assertSameValuesAnyOrder( int[] expected, int[] actual )
{
assertEquals("length",expected.length,actual.length);
final int len = expected.length;
Set values = new HashSet();
for( int i=0; i<len; i++ ) {
values.add( Integer.valueOf( expected[ i ] ) );
}
if( values.size() != expected.length ) {
throw new AssertionError("duplicate values in 'expected'.");
}
for( int i=0; i<len; i++ ) {
int value = actual[ i ];
if( ! values.remove( Integer.valueOf( value ) ) ) {
fail( "actual["+i+"]="+value+", but that value is not in expected[].");
}
}
}
/**
* Tests of the update to the internal matrix M[u,v]. This matrix maintains
* the #of distinct paths from u to v based on the edges in the directed
* graph, W.
* <p>
* Note: These tests are written directly using the
* {@link TxDag#updateClosure(int, int, boolean)} method, so the matrix W is
* not actually updated.
*/
public void test_updateClosure_001()
{
final int CAPACITY = 2;
TxDag dag = new TxDag( CAPACITY );
// declare tx0 and tx1 and verify expected index assignments.
final int tx0 = dag.lookup("tx0",true);
final int tx1 = dag.lookup("tx1",true);
assertEquals("tx0",0,tx0);
assertEquals("tx1",1,tx1);
// force display of all vertices.
dag.inbound[tx0] = 1;
dag.inbound[tx1] = 1;
dag.resetOrder();
/* W: (empty)
*
* M || 0 | 1
* --++---+---
* 0 || 0 | 0
* 1 || 0 | 0
*/
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,0},
{0,0}},
dag.getPathCountMatrix()
);
/* W:
* 0 -> 1
*
* M || 0 | 1
* --++---+---
* 0 || 0 | 1
* 1 || 0 | 0
*/
assertTrue( "addEdge", dag.updateClosure(tx0,tx1,true) ); // tx0 -> tx1 (aka tx0 WAITS_FOR tx1)
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,1},
{0,0}},
dag.getPathCountMatrix()
);
/*
* Add another edge which results in a deadlock. Verify that at least
* one cell on the diagonal in M is now positive.
*/
assertFalse( "addEdge", dag.updateClosure(tx1,tx0,true) ); // tx1 -> tx0
log.info( dag.toString() );
int nnzero = 0;
for( int i=0; i<CAPACITY; i++ ) {
if( dag.getPathCount(i,i) > 0 ) {
nnzero++;
}
}
if( nnzero == 0 ) {
fail( "No non-zero elements were found on the diagonal of M.");
}
}
/**
* A sequence of tests of the internal state of the {@link TxDag} with a
* capacity of <code>4</code> after adding an edge.
*/
public void test_updateClosure_002()
{
final int CAPACITY = 4;
TxDag dag = new TxDag( CAPACITY );
// declare transactions and verify expected index assignments.
final int tx0 = dag.lookup("tx0",true);
final int tx1 = dag.lookup("tx1",true);
final int tx2 = dag.lookup("tx2",true);
final int tx3 = dag.lookup("tx3",true);
assertEquals("tx0",0,tx0);
assertEquals("tx1",1,tx1);
assertEquals("tx2",2,tx2);
assertEquals("tx3",3,tx3);
// force display of all vertices.
dag.inbound[tx0] = 1;
dag.inbound[tx1] = 1;
dag.inbound[tx2] = 1;
dag.inbound[tx3] = 1;
dag.resetOrder();
/* W: (empty)
*
* M || 0 | 1 | 2 | 3
* --++---+---+---+---
* 0 || 0 | 0 | 0 | 0
* 1 || 0 | 0 | 0 | 0
* 2 || 0 | 0 | 0 | 0
* 3 || 0 | 0 | 0 | 0
*/
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,0,0,0},
{0,0,0,0},
{0,0,0,0},
{0,0,0,0}},
dag.getPathCountMatrix()
);
/* W:
* 0 -> 1
*
* M || 0 | 1 | 2 | 3
* --++---+---+---+---
* 0 || 0 | 1 | 0 | 0
* 1 || 0 | 0 | 0 | 0
* 2 || 0 | 0 | 0 | 0
* 3 || 0 | 0 | 0 | 0
*/
assertTrue( "addEdge", dag.updateClosure(tx0,tx1,true) ); // tx0 -> tx1 (aka tx0 WAITS_FOR tx1)
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,1,0,0},
{0,0,0,0},
{0,0,0,0},
{0,0,0,0}},
dag.getPathCountMatrix()
);
/* W:
* 0 -> 1
* 1 -> 2
*
* M || 0 | 1 | 2 | 3
* --++---+---+---+---
* 0 || 0 | 1 | 1 | 0
* 1 || 0 | 0 | 1 | 0
* 2 || 0 | 0 | 0 | 0
* 3 || 0 | 0 | 0 | 0
*/
assertTrue( "addEdge", dag.updateClosure(tx1,tx2,true) );
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,1,1,0},
{0,0,1,0},
{0,0,0,0},
{0,0,0,0}},
dag.getPathCountMatrix()
);
/* Remove the tx1->tx2 edge and verify that the prior state of M is
* recovered.
*
* W:
* 0 -> 1
*
* M || 0 | 1 | 2 | 3
* --++---+---+---+---
* 0 || 0 | 1 | 0 | 0
* 1 || 0 | 0 | 0 | 0
* 2 || 0 | 0 | 0 | 0
* 3 || 0 | 0 | 0 | 0
*/
assertTrue( "removeEdge", dag.updateClosure(tx1,tx2,false) );
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,1,0,0},
{0,0,0,0},
{0,0,0,0},
{0,0,0,0}},
dag.getPathCountMatrix()
);
/* Add tx1->tx2 back in.
*
* W:
* 0 -> 1
* 1 -> 2
*
* M || 0 | 1 | 2 | 3
* --++---+---+---+---
* 0 || 0 | 1 | 1 | 0
* 1 || 0 | 0 | 1 | 0
* 2 || 0 | 0 | 0 | 0
* 3 || 0 | 0 | 0 | 0
*/
assertTrue( "addEdge", dag.updateClosure(tx1,tx2,true) );
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,1,1,0},
{0,0,1,0},
{0,0,0,0},
{0,0,0,0}},
dag.getPathCountMatrix()
);
/* Remove the tx0->tx1 edge and verify the expected state for M.
*
* W:
* 1 -> 2
*
* M || 0 | 1 | 2 | 3
* --++---+---+---+---
* 0 || 0 | 0 | 0 | 0
* 1 || 0 | 0 | 1 | 0
* 2 || 0 | 0 | 0 | 0
* 3 || 0 | 0 | 0 | 0
*/
assertTrue( "removeEdge", dag.updateClosure(tx0,tx1,false) );
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,0,0,0},
{0,0,1,0},
{0,0,0,0},
{0,0,0,0}},
dag.getPathCountMatrix()
);
/* Remove both tx1->tx2 and verify that the M is all zeros.
*
* W: (empty)
*
* M || 0 | 1 | 2 | 3
* --++---+---+---+---
* 0 || 0 | 0 | 0 | 0
* 1 || 0 | 0 | 0 | 0
* 2 || 0 | 0 | 0 | 0
* 3 || 0 | 0 | 0 | 0
*/
assertTrue( "removeEdge", dag.updateClosure(tx1,tx2,false) );
log.info( dag.toString() );
assertSamePathCounts( new int[][]{
{0,0,0,0},
{0,0,0,0},
{0,0,0,0},
{0,0,0,0}},
dag.getPathCountMatrix()
);
}
/**
* Helper class represents the internal state of a {@link TxDag}
* instance and supports methods to compare the saved state with another
* {@link TxDag} instance.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan
* Thompson</a>
* @version $Id$
*/
class State
{
// The explicitly asserted edges for the produced state.
public final TxDag.Edge[] edges;
// The path count matrix for the produced state.
public final int[][] M;
public final int[] inbound;
public final int[] outbound;
public final Object[] transactions;
/**
* Constructor clones the internal state of the {@link TxDag}.
*
* @param dag The graph.
*/
public State( final TxDag dag )
{
this.edges = dag.getEdges(false);
this.M = dag.getPathCountMatrix();
this.inbound = (int[])dag.inbound.clone();
this.outbound = (int[])dag.outbound.clone();
this.transactions = (Object[])dag.transactions.clone();
}
/**
* Verify that <i>dag</i> has a state consistent with this
* historical state.
*/
public void assertSameState( TxDag dag )
{
assertSameEdges( edges, dag.getEdges(false) );
assertSamePathCounts(M, dag.getPathCountMatrix());
assertSameValues(inbound, dag.inbound);
assertSameValues(outbound, dag.outbound);
assertEquals("#transactions", transactions.length, dag.transactions.length );
for( int i=0; i<transactions.length; i++ ) {
assertEquals("transactions["+i+"]", transactions[i], dag.transactions[i]);
}
}
} // class State.
/**
* Implements the performance test for {@link #testSymmetricOperations()}.
* <p>
* Performs random additive operations on the DAG until a deadlock results.
* The initial state and the state after each additive operation is stored.
* Once a deadlock is reached, verifies that the last stored state is still
* valid (deadlock should not update the state of the DAG) and then performs
* the inverse of each of the addition operations in the reverse order in
* which they were applied. After each inverse operation, verifies that the
* state of the DAG corresponds to the state before the corresponding
* additive operation.
* <p>
* The additive operations and their inverses are:
* <ul>
* <li>insert/remove vertex</li>
* <li>insert/remove edge</li>
* </ul>
* A vertex corresponds to a transaction. Creating a vertex therefore
* corresponds to the action of starting a new transaction. Likewise
* removing a vertex corresponds to the action of terminating a transaction
* (either by aborting the transaction or committing the transaction).
*
* @param r
* A random number generator.
* @param dag
* A dag with a maximum capacity.
*/
public void doSymmetricOperationsTest( Random r, TxDag dag )
{
// Code for "no action".
final int NO_ACTION = -1;
// Code for action that creates a new vertex.
final int INSERT_VERTEX = 0;
// Code for action that creates an edge between two existing vertices.
final int INSERT_EDGE = 1;
// Capacity of the DAG.
final int capacity = dag.capacity();
/**
* Helper class records an action and the state that it produced.
*/
class ActionState extends State
{
// The action that produced the state.
public final int action;
// Vertex inserted by an INSERT_VERTEX action.
public final Object vertex;
// Source and target of an INSERT_EDGE action.
public final Object src, tgt;
/**
* Constructor used for the initial state (no action).
*/
ActionState( TxDag dag )
{
super(dag);
this.action = NO_ACTION;
this.vertex = null;
this.src = null;
this.tgt = null;
}
/**
* Constructor used for INSERT_VERTEX action.
* @param action INSERT_VERTEX
* @param vertex The new vertex.
* @param dag
*/
ActionState( int action, Object vertex, TxDag dag )
{
super(dag);
this.action = action;
this.vertex = vertex;
this.src = null;
this.tgt = null;
}
/**
* Constructor used for INSERT_EDGE action.
* @param action INSERT_EDGE
* @param src The source of the edge.
* @paramm tgt The target of the edge.
* @param dag
*/
ActionState( int action, Object src, Object tgt, TxDag dag )
{
super(dag);
this.action = action;
this.vertex = null;
this.src = src;
this.tgt = tgt;
}
};
// Vector of states for the DAG together with the action which produced that state.
/*
* Run the state machine forward adding vertices and edges randomly
* until a deadlock results.
*/
Vector history = new Vector();
Vector vertices = new Vector();
boolean done = false; // set true to terminate this loop.
history.add( new ActionState( dag ) ); // record initial state ("NO_ACTION").
while( ! done ) {
/*
* Probability of inserting an edge, which corresponds to the
* probability of one transaction waiting on another.
*
* P(insertVertex) := 1 - p(insertEdge).
*/
final int size = dag.size();
// Note: This could be an inverse function of the #of vertices.
final float pInsertEdge = .3f;
// Random number used to choose the action to take.
float rand = r.nextFloat();
if( ( size == capacity ) || ( dag.size() >= 2 && rand < pInsertEdge ) ) {
/*
* Insert edge. We always insert an edge if the DAG is at
* capacity (no more vertices may be declared). We never insert
* an edge unless at least two vertices have been defined.
*
* Choose two vertices randomly from those currently defined.
* The first will be the source of the edge and the 2nd will be
* its target. If source == target, then choose another target
* (since a transaction may not wait on itself).
*
* This is done repeatedly until either a deadlock results or an
* edge is added. (If the edge described already exists then we
* redo the selection and attempt to add another edge.)
*/
while (true) {
// Choose source vertex.
Object src = vertices.get(r.nextInt(size));
// Choose target vertex (target != source).
Object tgt;
do {
tgt = vertices.get(r.nextInt(size));
} while (tgt == src);
// Add edge.
try {
/*
* Attempt to add the edge. This will either succeed or
* fail. There are two failure conditions: (1) the edge
* would result in a deadlock; and (2) the edge already
* exists.
*/
log.info("adding edge: src="+src+", tgt="+tgt);
dag.addEdge(src, tgt);
history.add(new ActionState(INSERT_EDGE, src, tgt, dag));
break; // exit inner loop.
} catch (IllegalStateException ex) {
// Choose new source and target since this edge already
// exists.
log.warn("edge exists: src="+src+", tgt="+tgt);
continue; // repeat inner loop.
} catch (DeadlockException ex) {
/*
* Adding this edge results in a deadlock. Verify that
* the state of the DAG was NOT modified and then break
* out of the additive loop so that we can start the
* subtractive loop.
*/
log.warn("deadlock results: src="+src+", tgt="+tgt);
// verify no change in DAG state.
((ActionState)history.get(history.size()-1)).assertSameState(dag);
done = true; // exit outer loop.
break; // exit inner loop.
}
}
} else {
/*
* Insert vertex. We always take this action if there are less
* than two vertices since we must have two vertices defined to
* insert an edge.
*/
String tx = "tx"+size;
log.info("adding vertex: vertex="+tx);
dag.lookup(tx, true);
vertices.add( tx );
history.add( new ActionState(INSERT_VERTEX,tx,dag));
}
}
log.info("created history of " + (history.size() - 1)
+ " actions resulting in " + dag.size() + " vertices and "
+ dag.getEdges(false).length + " edges.");
/*
* Now take each action that we ran in reverse and run the action which
* is its inverse. E.g., if we added a vertex, then remove that vertex
* and if we added an edge, then remove that edge. After each inverse
* action verify that the new state of the DAG is consistent with the
* historical state of the DAG before we took the action whose effects
* were just undone by the inverse action.
*/
for (int i = history.size()-1; i > 0; i--) {
ActionState current = (ActionState) history.get(i);
ActionState prior = (ActionState) history.get(i - 1);
switch (current.action) {
case NO_ACTION:
throw new AssertionError();
case INSERT_VERTEX: {
log.info("removing vertex: "+current.vertex);
dag.removeEdges(current.vertex, false );
break;
}
case INSERT_EDGE: {
log.info("removing edge: src="+current.src+", tgt="+current.tgt);
dag.removeEdge(current.src, current.tgt);
break;
}
}
// verify that the inverse action restored the expected prior state.
prior.assertSameState(dag);
}
}
/**
* Compares two path count matrices for equality.
*
* @param expected
* An int[][] matrix with at least two rows and two columns.
* @param actual
* Another int[][] matrix with the same dimensions.
*/
public void assertSamePathCounts( int[][] expected, int[][] actual )
{
int nrows = expected.length;
int ncols = expected[ 0 ].length;
assertEquals("rows", nrows, actual.length );
assertEquals("cols", ncols, actual[0].length );
for( int i=0; i<nrows; i++ ) {
for( int j=0; j<ncols; j++ ) {
assertEquals( "M["+i+","+j+"]", expected[i][j], actual[i][j] );
}
}
}
/**
* Compares two Edge[]s and verifies that the same edges are defined without
* regard to order.
*
* @param expected The expected Edge[].
* @param actual The actual Edge[].
*/
public void assertSameEdges( Edge[] expected, Edge[] actual )
{
// verify arguments.
if( expected == null ) {
throw new IllegalArgumentException("expected is null");
}
if( actual == null ) {
fail("actual is null.");
}
// clone since we will modify expected[].
expected = (Edge[])expected.clone();
// verify length.
assertEquals("length",expected.length,actual.length);
final int len = expected.length;
// make sure there are no null elements.
for( int i=0; i<len; i++ ) {
if( expected[ i ] == null ) {
throw new IllegalArgumentException("expected["+i+"] is null." );
}
if( actual[ i ] == null ) {
fail("actual["+i+"] is null." );
}
}
/*
* For each element in [actual], scan the [expected] edges. If no match
* is found, then fail. Otherwise clear the match from [expected] and
* repeat for the next element in [actual].
*/
for( int i=0; i<len; i++ ) {
Edge actualEdge = actual[ i ];
boolean matched = false;
for( int j=0; j<len; j++ ) {
Edge expectedEdge = expected[ j ];
if( expectedEdge == null ) continue; // already matched.
if( expectedEdge.src == actualEdge.src && expectedEdge.tgt == actualEdge.tgt ) {
expected[ j ] = null; // clear from expected since we just matched.
matched = true;
break; // next actual edge.
}
}
if( ! matched ) {
fail("unexpected edge: src="+actualEdge.src+", tgt="+actual[i].tgt);
}
}
}
/**
* Some tests to verify {@link #assertSameEdges(Edge[], Edge[])}.
*/
public void testAssertSameEdges()
{
/*
* Some vertices. Note that vertices get compared by _reference_ and
* MUST be the same instance.
*/
String tx1 = "tx1";
String tx2 = "tx2";
String tx3 = "tx3";
String tx4 = "tx4";
/*
* Some edges.
*/
Edge e1 = new Edge(tx1,tx2,false);
Edge e2 = new Edge(tx2,tx1,false);
Edge e3 = new Edge(tx4,tx3,false);
Edge e4 = new Edge(tx1,tx3,false);
Edge e5 = new Edge(tx2,tx3,false);
Edge e6 = new Edge(tx4,tx1,false);
// Test correct rejection of illegal arguments.
try {
assertSameEdges( null, new Edge[]{} );
fail("Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
// Test correct rejection of illegal arguments.
try {
assertSameEdges( new Edge[]{null}, new Edge[]{e1} );
fail("Expecting exception: "+IllegalArgumentException.class);
}
catch( IllegalArgumentException ex ) {
log.info("Expected exception: "+ex);
}
// Test for correct acceptance when edge[]s are consistent.
assertSameEdges( new Edge[]{}, new Edge[]{} );
assertSameEdges( new Edge[]{e1}, new Edge[]{e1} );
assertSameEdges( new Edge[]{e2}, new Edge[]{e2} );
assertSameEdges( new Edge[]{e1,e2}, new Edge[]{e1,e2} );
assertSameEdges( new Edge[]{e2,e1}, new Edge[]{e1,e2} );
assertSameEdges( new Edge[]{e1,e2}, new Edge[]{e2,e1} );
assertSameEdges( new Edge[]{e1,e2,e3,e4,e5,e6}, new Edge[]{e2,e3,e6,e4,e1,e5} );
// Test correct failure for actual not consistent with expected.
try {
assertSameEdges( new Edge[]{e1,e2}, new Edge[]{e1} );
throw new RuntimeException("Expecting "+AssertionFailedError.class);
}
catch( AssertionFailedError ex ) {
log.info("Expected exception: "+ex);
}
try {
assertSameEdges( new Edge[]{e1}, new Edge[]{e2} );
throw new RuntimeException("Expecting "+AssertionFailedError.class);
}
catch( AssertionFailedError ex ) {
log.info("Expected exception: "+ex);
}
}
/**
* Verify that we can recycle the internal transaction identifiers when a
* transaction is removed from the DAG (either through abort or commit
* actions).
* <p>
* Transaction objects are application defined. They are mapped into indices
* for the internal arrays by {@link TxDag#lookup(Object, boolean)}. Those
* indices must be released for reuse by {@link TxDag#releaseVertex(Object)}
* of the capacity of the graph will be exhausted. This test verifies that
* they are.
* <p>
* The test creates and removes vertices repeatedly and verifies that we can
* create more vertices than would be allowed for by the capacity (therefore
* suggesting that vertices are being recycled correctly).
*/
public void test_recyclingIndices()
{
final int CAPACITY = 10;
final TxDag dag = new TxDag(CAPACITY);
for( int i=0; i<CAPACITY*2; i++ ) {
String tx = "tx"+i;
dag.lookup(tx, true );
dag.removeEdges(tx, false );
}
}
/**
* Verify that the DAG state is correctly updated when adding a variety of
* WAITS_FOR relationships that do NOT form cycles.
*/
public void test_noCycles_001()
{
final int CAPACITY = 5;
final TxDag dag = new TxDag( CAPACITY );
final String tx0 = "tx0";
final String tx1 = "tx1";
final String tx2 = "tx2";
final String tx3 = "tx3";
final String tx4 = "tx4";
// add edges of the form: src WAITS_FOR tgt.
dag.addEdge(tx0, tx1);
dag.addEdge(tx1, tx2);
dag.addEdge(tx3, tx2);
dag.addEdge(tx4, tx1);
dag.addEdge(tx4, tx3);
assertSameEdges(new Edge[] { new Edge(tx0, tx1, false),
new Edge(tx1, tx2, false), new Edge(tx3, tx2, false),
new Edge(tx4, tx1, false), new Edge(tx4, tx3, false) }, dag
.getEdges(false));
}
/**
* Verify that the DAG state is correctly updated when adding a variety of
* WAITS_FOR relationships that do NOT form cycles (using the batch
* operation to add edges).
*/
public void test_noCycles__batch_001()
{
final int CAPACITY = 5;
final TxDag dag = new TxDag( CAPACITY );
final String tx0 = "tx0";
final String tx1 = "tx1";
final String tx2 = "tx2";
final String tx3 = "tx3";
final String tx4 = "tx4";
// add edges of the form: src WAITS_FOR {tgt}.
dag.addEdges(tx0, new Object[]{tx1,tx2,tx4});
dag.addEdges(tx4, new Object[]{tx3});
}
/**
* The first in a series of simple tests which verify that the DAG is
* correctly detecting updates when a set of new edges would result in a
* cycle.
*/
public void test_deadlock_001()
{
final int CAPACITY = 5;
final TxDag dag = new TxDag( CAPACITY );
final String tx0 = "tx0";
final String tx1 = "tx1";
// add edges of the form: src WAITS_FOR tgt.
dag.addEdge(tx0, tx1);
try {
dag.addEdge(tx1, tx0);
fail("Expecting exception: "+DeadlockException.class);
}
catch( DeadlockException ex ) {
log.info("Expected exception: "+ex);
}
}
public void test_deadlock_002()
{
final int CAPACITY = 5;
final TxDag dag = new TxDag( CAPACITY );
final String tx0 = "tx0";
final String tx1 = "tx1";
final String tx2 = "tx2";
final String tx3 = "tx3";
// add edges of the form: src WAITS_FOR tgt.
dag.addEdge(tx0, tx1);
dag.addEdge(tx2, tx1);
dag.addEdge(tx3, tx2);
try {
dag.addEdge(tx1, tx3);
fail("Expecting exception: "+DeadlockException.class);
}
catch( DeadlockException ex ) {
log.info("Expected exception: "+ex);
}
}
public void test_deadlock_003()
{
final int CAPACITY = 5;
final TxDag dag = new TxDag( CAPACITY );
final String tx0 = "tx0";
final String tx1 = "tx1";
final String tx2 = "tx2";
final String tx3 = "tx3";
// add edges of the form: src WAITS_FOR tgt.
dag.addEdge(tx0, tx1);
dag.addEdge(tx1, tx2);
dag.addEdge(tx2, tx3);
try {
dag.addEdge(tx3, tx1);
fail("Expecting exception: "+DeadlockException.class);
}
catch( DeadlockException ex ) {
log.info("Expected exception: "+ex);
}
}
public void test_deadlock_batch_001()
{
final int CAPACITY = 5;
final TxDag dag = new TxDag( CAPACITY );
final String tx0 = "tx0";
final String tx1 = "tx1";
final String tx2 = "tx2";
final String tx3 = "tx3";
// add edges of the form: src WAITS_FOR {tgt}.
// dag.addEdges(tx0, new Object[]{tx3,tx1,tx2});
dag.addEdge(tx0,tx3);
dag.addEdge(tx0,tx1);
dag.addEdge(tx0,tx2);
assertSameEdges(new Edge[] { new Edge(tx0, tx3, true),
new Edge(tx0, tx1, true), new Edge(tx0, tx2, true) }, dag
.getEdges(false));
try {
dag.addEdges(tx3, new Object[]{tx0});
// System.err.println(""+dag);
// dag.addEdge(tx3, tx0);
// System.err.println(""+dag);
fail("Expecting exception: "+DeadlockException.class);
}
catch( DeadlockException ex ) {
log.info("Expected exception: "+ex);
}
}
/**
* <p>
* Test adds N random edges to the graph and then removes them and verifies
* that removal correctly reproduces each intermediate state following an
* edge addition. Edges are added until a deadlock results. We verify that
* the deadlock did not update the internal matrix M and then backup state
* by state removing each edge in the reverse order and verifying that the
* correct state is reproduced as the edge is removed.
* </p>
* <p>
* Note: This test uses "small" matrices (20 vertices) to keep the memory
* footprint down since it makes a copy of the state of the DAG after each
* action. As the capacity of the graph goes up, this test will begin to
* stress the garbage collector so more trials and moderate capacity makes
* more sense.
* </p>
*
* @see #doSymmetricOperationsTest(Random, TxDag)
*/
public void testSymmetricOperations()
{
final int NTRIALS = 30;
final int CAPACITY = 20;
Random r = new Random();
for( int i=0; i<NTRIALS; i++ ) {
doSymmetricOperationsTest( r, new TxDag( CAPACITY ) );
}
}
}