/**
* Copyright (c) 2002-2012 "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.kernel.impl.cache;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.neo4j.helpers.collection.IteratorUtil.count;
import static org.neo4j.helpers.collection.MapUtil.stringMap;
import static org.neo4j.test.TargetDirectory.forTest;
import java.util.concurrent.CountDownLatch;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.kernel.impl.MyRelTypes;
import org.neo4j.test.subprocess.BreakPoint;
import org.neo4j.test.subprocess.BreakpointHandler;
import org.neo4j.test.subprocess.BreakpointTrigger;
import org.neo4j.test.subprocess.DebugInterface;
import org.neo4j.test.subprocess.DebuggedThread;
import org.neo4j.test.subprocess.EnabledBreakpoints;
import org.neo4j.test.subprocess.ForeignBreakpoints;
import org.neo4j.test.subprocess.SubProcessTestRunner;
@ForeignBreakpoints( {
@ForeignBreakpoints.BreakpointDef( type = "org.neo4j.kernel.impl.core.NodeImpl", method = "updateSize" )
} )
@RunWith( SubProcessTestRunner.class )
public class TestGcrCacheRemoveSizeDiverge
{
private static EmbeddedGraphDatabase graphdb;
private static DebuggedThread thread;
private static CountDownLatch latch = new CountDownLatch( 1 );
@BeforeClass
public static void startDb()
{
try
{
graphdb = new EmbeddedGraphDatabase( forTest(
TestGcrCacheRemoveSizeDiverge.class ).graphDbDir( true ).getAbsolutePath(),
stringMap( GraphDatabaseSettings.cache_type.name(), GCResistantCacheProvider.NAME ) );
}
catch ( Throwable t )
{
t.printStackTrace();
}
}
@AfterClass
public static void shutdownDb()
{
try
{
if ( graphdb != null ) graphdb.shutdown();
}
finally
{
graphdb = null;
}
}
private Node createNodeWithSomeRelationships()
{
Transaction tx = graphdb.beginTx();
try
{
Node node = graphdb.createNode();
for ( int i = 0; i < 10; i++ )
node.createRelationshipTo( node, MyRelTypes.TEST );
tx.success();
return node;
}
finally
{
tx.finish();
}
}
@BreakpointHandler( "updateSize" )
public static void onUpdateSize( BreakPoint self, DebugInterface di )
{
self.disable();
thread = di.thread().suspend( null );
latch.countDown();
}
@BreakpointHandler( "resumeUpdateSize" )
public static void onResumeUpdateSize( BreakPoint self, DebugInterface di )
{
thread.resume();
}
@BreakpointTrigger( "resumeUpdateSize" )
private void resumeUpdateSize() {}
@BreakpointTrigger( "enableBreakpoints" )
private void enableBreakpoints() {}
@BreakpointHandler( "enableBreakpoints" )
public static void onEnableBreakpoints( @BreakpointHandler( "updateSize" ) BreakPoint updateSize, DebugInterface di )
{
updateSize.enable();
}
@Test
@EnabledBreakpoints( { "enableBreakpoints", "resumeUpdateSize" } )
public void removeFromCacheInBetweenOtherThreadStateChangeAndUpdateSize() throws Exception
{ // ...should yield a size which matches actual cache size
/* Here's the English version of how to trigger it:
* T1: create node N with 10 relationships
* T1: clear cache (simulating that it needs to be loaded for the first time the next access)
* T1: request N so that it gets put into cache (no relationships loaded)
* T1: load relationships of N, break right before call to NodeImpl#updateSize
* T2: remove N from cache, which calls N.size() which will return a size different from
* what the cache thinks that object is so it will subtract more than it should.
* T1: resume execution
*
* => cache size should be 0, but the bug makes it less than zero. Over time the cache will
* diverge more and more from actual cache size.
*/
final Node node = createNodeWithSomeRelationships();
graphdb.getNodeManager().clearCache();
enableBreakpoints();
graphdb.getNodeById( node.getId() );
final Cache<?> nodeCache = graphdb.getNodeManager().caches().iterator().next();
assertTrue( "We didn't get a hold of the right cache object", nodeCache.toString().toLowerCase().contains( "node" ) );
Thread t1 = new Thread( "T1: Relationship loader" )
{
@Override
public void run()
{
// It will break in NodeImpl#loadInitialRelationships right before calling updateSize
count( node.getRelationships() );
}
};
t1.start();
// TODO wait for latch instead, but it seems to be a different instance than the one we countDown.
// latch.await();
Thread.sleep( 2000 );
Thread t2 = new Thread( "T2: Cache remover" )
{
@Override
public void run()
{
nodeCache.remove( node.getId() );
}
};
t2.start();
t2.join();
resumeUpdateSize();
t1.join();
assertEquals( "Invalid cache size for " + nodeCache, 0, nodeCache.size() );
}
}