/**
* Copyright (c) 2002-2010 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.index.lucene;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.index.IndexHits;
import org.neo4j.index.IndexService;
import org.neo4j.index.Neo4jWithIndexTestCase;
/**
* This test is abstract because it takes a while to run. It belongs in a QA project
* instead really...
*/
public abstract class TestLuceneIndexManyThreads extends Neo4jWithIndexTestCase
{
private AtomicInteger COUNT_CREATES = new AtomicInteger();
private AtomicInteger COUNT_DELETES = new AtomicInteger();
private AtomicInteger COUNT_READS = new AtomicInteger();
@Override
protected IndexService instantiateIndex()
{
return new LuceneIndexService( graphDb() );
}
@Test
public void tryToBreak() throws Exception
{
Node rootNode = graphDb().createNode();
restartTx();
Collection<WorkerThread> threads = new ArrayList<WorkerThread>();
long endTime = System.currentTimeMillis() + 1000 * 30;
List<Long> aliveNodes = Collections.synchronizedList(
new ArrayList<Long>() );
for ( int i = 0; i < 20; i++ )
{
WorkerThread thread =
new WorkerThread( rootNode, endTime, aliveNodes );
threads.add( thread );
thread.start();
}
for ( WorkerThread thread : threads )
{
thread.join();
if ( thread.exception != null )
{
thread.exception.printStackTrace();
fail( thread.exception.getMessage() );
}
}
// System.out.println( "c:" + COUNT_CREATES.get() + ", d:" +
// COUNT_DELETES.get() + ", r:" + COUNT_READS.get() );
}
private enum RelTypes implements RelationshipType
{
TEST_TYPE,
}
private class WorkerThread extends Thread
{
private final Random random = new Random();
private final Node rootNode;
private final long endTime;
private final List<Long> aliveNodes;
private RuntimeException exception;
WorkerThread( Node rootNode, long endTime, List<Long> aliveNodes )
{
this.rootNode = rootNode;
this.endTime = endTime;
this.aliveNodes = aliveNodes;
}
@Override
public void run()
{
try
{
while ( System.currentTimeMillis() < endTime )
{
Collection<Long> createdIds = null;
Collection<Long> deletedIds = null;
Transaction tx = graphDb().beginTx();
try
{
int what = random.nextInt( 3 );
if ( what == 0 ) // Create stuff
{
createdIds = createStuff();
}
else if ( what == 1 ) // Delete stuff
{
deletedIds = deleteStuff();
}
else if ( what == 2 ) // Verify stuff
{
verifyStuff();
}
tx.success();
}
finally
{
tx.finish();
}
if ( createdIds != null )
{
aliveNodes.addAll( createdIds );
COUNT_CREATES.addAndGet( createdIds.size() );
}
if ( deletedIds != null )
{
aliveNodes.removeAll( deletedIds );
COUNT_DELETES.addAndGet( deletedIds.size() );
}
}
}
catch ( RuntimeException e )
{
this.exception = e;
throw e;
}
}
private void set( Node node, String key, Object value )
{
node.setProperty( key, value );
index().index( node, key, value );
}
private Collection<Long> createStuff()
{
int count = random.nextInt( 1000 ) + 1;
Collection<Long> ids = new ArrayList<Long>();
for ( int i = 0; i < count; i++ )
{
Node node = graphDb().createNode();
rootNode.createRelationshipTo( node, RelTypes.TEST_TYPE );
set( node, "type", "TYPE" );
set( node, "name", "user" + random.nextInt( 10000 ) );
if ( random.nextBoolean() )
{
set( node, "sometimes", random.nextInt( 10 ) );
}
ids.add( node.getId() );
}
return ids;
}
private Collection<Long> deleteStuff()
{
if ( aliveNodes.size() < 2000 )
{
return Collections.emptySet();
}
int count = random.nextInt( 3 ) + 1;
Collection<Long> ids = new ArrayList<Long>();
for ( int i = 0; i < count; i++ )
{
Node node = getRandomAliveNode();
if ( node == null )
{
continue;
}
for ( String key : node.getPropertyKeys() )
{
index().removeIndex( node, key,
node.getProperty( key ) );
}
node.getSingleRelationship( RelTypes.TEST_TYPE,
Direction.INCOMING ).delete();
node.delete();
ids.add( node.getId() );
}
return ids;
}
private Node getRandomAliveNode()
{
if ( aliveNodes.isEmpty() )
{
return null;
}
return graphDb().getNodeById( aliveNodes.get( random.nextInt(
aliveNodes.size() ) ) );
}
private void verifyStuff()
{
// Test to get an iterator for the "type" property
// (all nodes have this)
// if ( aliveNodes.size() > 3000 )
// {
// IndexHits<Node> sometimesHits = indexService.getNodes(
// "sometimes", 5 );
// assertTrue( isRoughly( aliveNodes.size() / 20,
// sometimesHits.size() ) );
// }
IndexHits<Node> hits = index().getNodes( "type", "TYPE" );
for ( Node hit : hits )
{
hit.getProperty( "name" );
COUNT_READS.incrementAndGet();
}
// Test one random node
Node node = getRandomAliveNode();
if ( node != null )
{
String name = ( String ) node.getProperty( "name" );
boolean found = false;
for ( Node hit : index().getNodes( "name", name ) )
{
COUNT_READS.incrementAndGet();
if ( hit.equals( node ) )
{
found = true;
}
}
assertTrue( found );
}
}
// private boolean isRoughly( int shouldBeRoughly, int value )
// {
// // Ok so within +-20%
// int max = ( int ) ( shouldBeRoughly * 1.2d );
// int min = ( int ) ( shouldBeRoughly * 0.8d );
// return value >= min && value <= max;
// }
}
}