/**
* 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.ha;
import static org.junit.Assert.assertTrue;
import java.util.concurrent.CountDownLatch;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.neo4j.graphdb.DynamicRelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.factory.HighlyAvailableGraphDatabaseFactory;
import org.neo4j.kernel.ha.HaSettings;
import org.neo4j.kernel.ha.HighlyAvailableGraphDatabase;
import org.neo4j.test.TargetDirectory;
import org.neo4j.test.subprocess.BreakPoint;
import org.neo4j.test.subprocess.BreakPoint.Event;
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.com.Client",
method = "makeSureNextTransactionIsFullyFetched", on = Event.ENTRY),
@ForeignBreakpoints.BreakpointDef(type = "org.neo4j.com.DechunkingChannelBuffer", method = "readNextChunk",
on = Event.EXIT)})
@RunWith(SubProcessTestRunner.class)
@Ignore("This test depends on chuncked requests, otherwise it will hang. So either reduce the Protocol" +
".DEFAULT_FRAME_LENGTH to 1024"
+ "or create a huge difference in the stores between master and slave which will lead to a multichunk " +
"response.")
public class TestClientThreadIsolation
{
@Test
@EnabledBreakpoints({"makeSureNextTransactionIsFullyFetched",
"readNextChunk", "waitTxCopyToStart", "finish"})
public void testTransactionsPulled() throws Exception
{
final HighlyAvailableGraphDatabase master = (HighlyAvailableGraphDatabase) new HighlyAvailableGraphDatabaseFactory().
newHighlyAvailableDatabaseBuilder( TargetDirectory.forTest( TestClientThreadIsolation.class ).directory(
"master", true ).getAbsolutePath() ).
setConfig( HaSettings.server_id, "1" ).
newGraphDatabase();
final HighlyAvailableGraphDatabase slave1 = (HighlyAvailableGraphDatabase) new HighlyAvailableGraphDatabaseFactory().
newHighlyAvailableDatabaseBuilder( TargetDirectory.forTest( TestClientThreadIsolation.class ).directory(
"slave1", true ).getAbsolutePath() ).
setConfig( HaSettings.server_id, "2" ).
setConfig( HaSettings.max_concurrent_channels_per_slave, "2" )
.setConfig( HaSettings.ha_server, "127.0.0.1:8001" )
.setConfig( HaSettings.cluster_server, "127.0.0.1:5002" )
.setConfig( HaSettings.initial_hosts, "127.0.0.1:5001" )
.newGraphDatabase();
Transaction masterTx = master.beginTx();
master.createNode().createRelationshipTo( master.createNode(),
DynamicRelationshipType.withName( "master" ) ).setProperty(
"largeArray", new int[20000] );
masterTx.success();
masterTx.finish();
Thread thread1 = new Thread( new Runnable()
{
public void run()
{
// TODO Figure out how to do this
// Master masterClient = slave1.getBroker().getMaster().first();
// Response<Integer> response = masterClient.createRelationshipType(
// slave1.getSlaveContext( 10 ), "name" );
// slave1.receive( response ); // will be suspended here
// response.close();
}
}, "thread 1" );
Thread thread2 = new Thread( new Runnable()
{
public void run()
{
/*
* We have two operations since we need to make sure this test passes
* before and after the proper channel releasing fix. The issue is
* that we can't have only one channel since it will deadlock because
* the txCopyingThread is suspended and won't release the channel
* (after the fix). But the problem is that with two channels going
* before the fix it won't break because the RR policy in
* ResourcePool will give the unused channel to the new requesting thread,
* thus not triggering the bug. The solution is to do two requests so
* eventually get the released, half consumed channel.
*/
// TODO Figure out how to do this
// try
// {
// waitTxCopyToStart();
// Master masterClient = slave1.getBroker().getMaster().first();
// SlaveContext ctx = slave1.getSlaveContext( 11 );
// Response<Integer> response = masterClient.createRelationshipType(
// ctx, "name2" );
// slave1.receive( response );
// response.close();
//
// // This will break before the fix
// response = masterClient.createRelationshipType(
// slave1.getSlaveContext( 12 ), "name3" );
// slave1.receive( response );
// response.close();
//
// /*
// * If the above fails, this won't happen. Used to fail the
// * test gracefully
// */
// Transaction masterTx = master.beginTx();
// master.getReferenceNode().createRelationshipTo(
// master.createNode(),
// DynamicRelationshipType.withName( "test" ) );
// masterTx.success();
// masterTx.finish();
// }
// finally
// {
// finish();
// }
}
}, "thread 2" );
thread1.start();
thread2.start();
thread1.join();
thread2.join();
assertTrue(
master.getReferenceNode().getRelationships(
DynamicRelationshipType.withName( "test" ) ).iterator().hasNext() );
}
private static DebuggedThread txCopyingThread;
private static DebuggedThread interferingThread;
/*
* Blocks the txCopyingThread after reading the
* first chunk but before moving on to the next one.
*/
private static CountDownLatch latch = new CountDownLatch( 1 );
@BreakpointTrigger("waitTxCopyToStart")
private void waitTxCopyToStart()
{
// wait for the first thread to grab the updates
}
@BreakpointTrigger("finish")
private void finish()
{
// resume the suspended thread
}
@BreakpointHandler("waitTxCopyToStart")
public static void onWaitTxCopyToStart( BreakPoint self, DebugInterface di )
{
interferingThread = di.thread().suspend( null );
latch.countDown();
}
@BreakpointHandler("finish")
public static void onFinish( BreakPoint self, DebugInterface di )
{
txCopyingThread.resume();
}
@BreakpointHandler("makeSureNextTransactionIsFullyFetched")
public static void onStartingStoreCopy( BreakPoint self, DebugInterface di,
@BreakpointHandler("readNextChunk") BreakPoint onReadNextChunk )
throws Exception
{
// Wait for the other thread to recycle the channel
latch.await();
txCopyingThread = di.thread();
self.disable();
}
@BreakpointHandler("readNextChunk")
public static void onReadNextChunk( BreakPoint self, DebugInterface di )
throws Exception
{
// Check because the interfering thread will trigger this too
if ( txCopyingThread != null
&& di.thread().name().equals( txCopyingThread.name() ) )
{
txCopyingThread.suspend( null );
interferingThread.resume();
self.disable();
}
}
}