/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.internal.cluster;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.neo4j.driver.internal.net.BoltServerAddress;
import org.neo4j.driver.internal.spi.Connection;
import org.neo4j.driver.internal.spi.ConnectionPool;
import org.neo4j.driver.internal.spi.PooledConnection;
import org.neo4j.driver.internal.util.Clock;
import org.neo4j.driver.internal.util.FakeClock;
import org.neo4j.driver.v1.Logger;
import org.neo4j.driver.v1.exceptions.ProtocolException;
import org.neo4j.driver.v1.exceptions.ServiceUnavailableException;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.A;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.B;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.C;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.D;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.E;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.EMPTY;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.F;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.VALID_CLUSTER_COMPOSITION;
import static org.neo4j.driver.internal.cluster.ClusterCompositionUtil.createClusterComposition;
import static org.neo4j.driver.internal.logging.DevNullLogger.DEV_NULL_LOGGER;
import static org.neo4j.driver.internal.net.BoltServerAddress.LOCAL_DEFAULT;
@RunWith( Enclosed.class )
public class RediscoveryTest
{
private static HostNameResolver directMapProvider = new HostNameResolver()
{
@Override
public Set<BoltServerAddress> resolve( BoltServerAddress initialRouter )
{
Set<BoltServerAddress> directMap = new HashSet<>();
directMap.add( initialRouter );
return directMap;
}
};
private static ClusterCompositionResponse.Success success( ClusterComposition cluster )
{
return new ClusterCompositionResponse.Success( cluster );
}
private static ClusterCompositionResponse.Failure failure( RuntimeException e )
{
return new ClusterCompositionResponse.Failure( e );
}
public static class RoutingSettingsTest
{
@Test
public void shouldTryConfiguredMaxRoutingFailures() throws Exception
{
// given
int maxRoutingFailures = 7;
RoutingSettings settings = new RoutingSettings( maxRoutingFailures, 10, null );
Clock clock = mock( Clock.class );
RoutingTable routingTable = new TestRoutingTable( A );
ClusterCompositionProvider mockedProvider = mock( ClusterCompositionProvider.class );
when( mockedProvider.getClusterComposition( any( Connection.class ) ) ).thenThrow( new RuntimeException() );
Rediscovery rediscovery = new Rediscovery( A, settings, clock, DEV_NULL_LOGGER, mockedProvider, directMapProvider );
// when
try
{
rediscovery.lookupClusterComposition( routingTable, mock( ConnectionPool.class ) );
fail("Should fail as failed to discovery");
}
catch( ServiceUnavailableException e )
{
assertThat( e.getMessage(), containsString( "No routing servers available" ) );
}
// then
verify( mockedProvider, times( maxRoutingFailures ) ).getClusterComposition( any( Connection.class ) );
}
}
public static class FailedToConnectTest
{
@Test
public void shouldForgetRouterAndTryNextRouterWhenFailedToConnect() throws Throwable
{
// Given
TestRoutingTable routingTable = new TestRoutingTable( A, B );
PooledConnection healthyConn = mock( PooledConnection.class );
ConnectionPool mockedConnections = mock( ConnectionPool.class );
when( mockedConnections.acquire( A ) ).thenThrow( new ServiceUnavailableException( "failed to connect" ) );
when( mockedConnections.acquire( B ) ).thenReturn( healthyConn );
ClusterCompositionProvider
mockedProvider = mock( ClusterCompositionProvider.class );
when( mockedProvider.getClusterComposition( healthyConn ) )
.thenReturn( success( VALID_CLUSTER_COMPOSITION ) );
// When
ClusterComposition clusterComposition = rediscover( mockedConnections, routingTable, mockedProvider );
// Then
assertThat( routingTable.removedRouters.size(), equalTo( 1 ) );
assertThat( routingTable.removedRouters.get( 0 ), equalTo( A ) );
assertThat( clusterComposition, equalTo( VALID_CLUSTER_COMPOSITION ) );
}
}
public static class ProcedureNotFoundTest
{
@Test
public void shouldThrowServiceUnavailableWhenNoProcedureFound() throws Throwable
{
// Given
RoutingTable routingTable = new TestRoutingTable( A );
PooledConnection healthyConn = mock( PooledConnection.class );
ConnectionPool mockedConnections = mock( ConnectionPool.class );
when( mockedConnections.acquire( A ) ).thenReturn( healthyConn );
ClusterCompositionProvider
mockedProvider = mock( ClusterCompositionProvider.class );
when( mockedProvider.getClusterComposition( healthyConn ) )
.thenReturn( failure( new ServiceUnavailableException( "No such procedure" ) ) );
// When & When
try
{
rediscover( mockedConnections, routingTable, mockedProvider );
fail( "Expecting a failure but not triggered." );
}
catch( Exception e )
{
assertThat( e, instanceOf( ServiceUnavailableException.class ) );
assertThat( e.getMessage(), startsWith( "No such procedure" ) );
}
}
}
@RunWith( Parameterized.class )
public static class NoWritersTest
{
@Parameters(name = "Rediscovery result: {0}")
public static Collection<Object[]> data() {
return asList(new Object[][] {
{"([A], [C], [])", createClusterComposition( asList( A ), EMPTY, asList( C ) )},
{"([A], [CD], [])", createClusterComposition( asList( A ), EMPTY, asList( C, D ) )},
{"([AB], [C], [])", createClusterComposition( asList( A, B ), EMPTY, asList( C ) )},
{"([AB], [CD], [])", createClusterComposition( asList( A, B ), EMPTY, asList( C, D ) )}
});
}
private ClusterComposition noWriters;
public NoWritersTest( String testName, ClusterComposition noWriters )
{
this.noWriters = noWriters;
}
@Test
public void shouldAcceptTableWithoutWriters() throws Throwable
{
// Given
RoutingTable routingTable = new TestRoutingTable( A );
PooledConnection noWriterConn = mock( PooledConnection.class );
ConnectionPool mockedConnections = mock( ConnectionPool.class );
when( mockedConnections.acquire( A ) ).thenReturn( noWriterConn );
ClusterCompositionProvider mockedProvider = mock( ClusterCompositionProvider.class );
when( mockedProvider.getClusterComposition( noWriterConn ) ).thenReturn( success( noWriters ) );
// When
ClusterComposition clusterComposition = rediscover( mockedConnections, routingTable, mockedProvider );
// Then
assertThat( clusterComposition, equalTo( noWriters ) );
}
@Test
public void shouldUseInitialRouterWhenRediscoveringAfterNoWriters() throws Throwable
{
// Given
RoutingTable routingTable = new TestRoutingTable( A, B, C );
PooledConnection noWriterConn = mock( PooledConnection.class );
PooledConnection initialRouterConn = mock( PooledConnection.class );
ConnectionPool mockedConnections = mock( ConnectionPool.class );
when( mockedConnections.acquire( A ) ).thenReturn( noWriterConn );
when( mockedConnections.acquire( B ) ).thenReturn( noWriterConn );
when( mockedConnections.acquire( C ) ).thenReturn( noWriterConn );
when( mockedConnections.acquire( F ) ).thenReturn( initialRouterConn );
ClusterCompositionProvider mockedProvider = mock( ClusterCompositionProvider.class );
when( mockedProvider.getClusterComposition( noWriterConn ) ).thenReturn( success( noWriters ) );
when( mockedProvider.getClusterComposition( initialRouterConn ) )
.thenReturn( success( VALID_CLUSTER_COMPOSITION ) );
Rediscovery rediscovery = new Rediscovery( F, new RoutingSettings( 1, 0 ), new FakeClock(),
DEV_NULL_LOGGER, mockedProvider, directMapProvider );
// first rediscovery should accept table with no writers
ClusterComposition composition1 = rediscovery.lookupClusterComposition( routingTable, mockedConnections );
// second rediscovery should ask initial router because previous routing table had no writers
ClusterComposition composition2 = rediscovery.lookupClusterComposition( routingTable, mockedConnections );
assertEquals( noWriters, composition1 );
assertEquals( VALID_CLUSTER_COMPOSITION, composition2 );
}
}
@RunWith( Parameterized.class )
public static class AtLeastOneOfEachTest
{
@Parameters(name = "Rediscovery result: {0}")
public static Collection<Object[]> data() {
return asList(new Object[][] {
{ "([A], [C], [E])", createClusterComposition( asList( A ), asList( C ), asList( E ) ) },
{ "([AB], [C], [E])", createClusterComposition( asList( A, B ), asList( C ), asList( E ) ) },
{ "([A], [CD], [E])", createClusterComposition( asList( A ), asList( C, D ), asList( E ) ) },
{ "([AB], [CD], [E])", createClusterComposition( asList( A, B ), asList( C, D ), asList( E ) ) },
{ "([A], [C], [EF])", createClusterComposition( asList( A ), asList( C ), asList( E, F ) ) },
{ "([AB], [C], [EF])", createClusterComposition( asList( A, B ), asList( C ), asList( E, F ) ) },
{ "([A], [CD], [EF])", createClusterComposition( asList( A ), asList( C, D ), asList( E, F ) ) },
{ "([AB], [CD], [EF])", createClusterComposition( asList( A, B ), asList( C, D ), asList( E, F ) )}
});
}
private ClusterComposition atLeastOneOfEach;
public AtLeastOneOfEachTest( String testName, ClusterComposition atLeastOneOfEach )
{
this.atLeastOneOfEach = atLeastOneOfEach;
}
@Test
public void shouldUpdateRoutingTableWithTheNewOne() throws Throwable
{
// Given
RoutingTable routingTable = new TestRoutingTable( A );
PooledConnection healthyConn = mock( PooledConnection.class );
ConnectionPool mockedConnections = mock( ConnectionPool.class );
when( mockedConnections.acquire( A ) ).thenReturn( healthyConn );
ClusterCompositionProvider
mockedProvider = mock( ClusterCompositionProvider.class );
when( mockedProvider.getClusterComposition( healthyConn ) ).thenReturn( success( atLeastOneOfEach ) );
// When
ClusterComposition clusterComposition = rediscover( mockedConnections, routingTable, mockedProvider );
// Then
assertThat( clusterComposition, equalTo( atLeastOneOfEach ) );
}
}
public static class IllegalResponseTest
{
@Test
public void shouldProtocolErrorWhenFailedToParseClusterComposition() throws Throwable
{
// Given
RoutingTable routingTable = new TestRoutingTable( A );
PooledConnection healthyConn = mock( PooledConnection.class );
ConnectionPool mockedConnections = mock( ConnectionPool.class );
when( mockedConnections.acquire( A ) ).thenReturn( healthyConn );
ClusterCompositionProvider mockedProvider = mock( ClusterCompositionProvider.class );
ProtocolException exception = new ProtocolException( "Failed to parse result" );
when( mockedProvider.getClusterComposition( healthyConn ) ).thenReturn( failure( exception ) );
// When & When
try
{
rediscover( mockedConnections, routingTable, mockedProvider );
fail( "Expecting a failure but not triggered." );
}
catch ( Exception e )
{
assertThat( e, instanceOf( ProtocolException.class ) );
assertThat( e, equalTo( (Exception) exception ) );
}
}
}
public static class InitialRouterTest
{
@Test
public void shouldNotTouchInitialRouterWhenSomePresentRouterResponds()
{
PooledConnection brokenConnection = mock( PooledConnection.class );
PooledConnection healthyConnection = mock( PooledConnection.class );
ConnectionPool connections = mock( ConnectionPool.class );
when( connections.acquire( B ) ).thenReturn( brokenConnection );
when( connections.acquire( C ) ).thenReturn( healthyConnection );
ClusterCompositionProvider clusterComposition = mock( ClusterCompositionProvider.class );
when( clusterComposition.getClusterComposition( brokenConnection ) )
.thenThrow( new ServiceUnavailableException( "Can't connect" ) );
when( clusterComposition.getClusterComposition( healthyConnection ) )
.thenReturn( success( VALID_CLUSTER_COMPOSITION ) );
RoutingTable routingTable = new TestRoutingTable( B, C );
ClusterComposition composition = rediscover( A, connections, routingTable, clusterComposition );
assertEquals( VALID_CLUSTER_COMPOSITION, composition );
verify( clusterComposition ).getClusterComposition( brokenConnection );
verify( clusterComposition ).getClusterComposition( healthyConnection );
verify( connections, never() ).acquire( A );
verify( connections ).acquire( B );
verify( connections ).acquire( C );
}
@Test
public void shouldUseInitialRouterWhenNoneOfExistingRoutersRespond()
{
PooledConnection healthyConnection = mock( PooledConnection.class );
PooledConnection brokenConnection1 = mock( PooledConnection.class );
PooledConnection brokenConnection2 = mock( PooledConnection.class );
ConnectionPool connections = mock( ConnectionPool.class );
when( connections.acquire( A ) ).thenReturn( healthyConnection );
when( connections.acquire( B ) ).thenReturn( brokenConnection1 );
when( connections.acquire( C ) ).thenReturn( brokenConnection2 );
ClusterCompositionProvider clusterComposition = mock( ClusterCompositionProvider.class );
when( clusterComposition.getClusterComposition( healthyConnection ) )
.thenReturn( success( VALID_CLUSTER_COMPOSITION ) );
when( clusterComposition.getClusterComposition( brokenConnection1 ) )
.thenThrow( new ServiceUnavailableException( "Can't connect" ) );
when( clusterComposition.getClusterComposition( brokenConnection2 ) )
.thenThrow( new ServiceUnavailableException( "Can't connect" ) );
RoutingTable routingTable = new TestRoutingTable( B, C );
ClusterComposition composition = rediscover( A, connections, routingTable, clusterComposition );
assertEquals( VALID_CLUSTER_COMPOSITION, composition );
verify( clusterComposition ).getClusterComposition( brokenConnection1 );
verify( clusterComposition ).getClusterComposition( brokenConnection2 );
verify( clusterComposition ).getClusterComposition( healthyConnection );
verify( connections ).acquire( A );
verify( connections ).acquire( B );
verify( connections ).acquire( C );
}
@Test
public void shouldUseInitialRouterWhenNoExistingRouters()
{
PooledConnection connection = mock( PooledConnection.class );
ConnectionPool connections = mock( ConnectionPool.class );
when( connections.acquire( A ) ).thenReturn( connection );
ClusterCompositionProvider clusterComposition = mock( ClusterCompositionProvider.class );
when( clusterComposition.getClusterComposition( connection ) )
.thenReturn( success( VALID_CLUSTER_COMPOSITION ) );
// empty routing table
RoutingTable routingTable = new TestRoutingTable();
ClusterComposition composition = rediscover( A, connections, routingTable, clusterComposition );
assertEquals( VALID_CLUSTER_COMPOSITION, composition );
verify( clusterComposition ).getClusterComposition( connection );
verify( connections ).acquire( A );
}
@Test
public void shouldNotUseInitialRouterTwiceIfRoutingTableContainsIt()
{
PooledConnection brokenConnection1 = mock( PooledConnection.class );
PooledConnection brokenConnection2 = mock( PooledConnection.class );
ConnectionPool connections = mock( ConnectionPool.class );
when( connections.acquire( A ) ).thenReturn( brokenConnection1 );
when( connections.acquire( B ) ).thenReturn( brokenConnection2 );
ClusterCompositionProvider clusterComposition = mock( ClusterCompositionProvider.class );
when( clusterComposition.getClusterComposition( brokenConnection1 ) )
.thenThrow( new ServiceUnavailableException( "Can't connect" ) );
when( clusterComposition.getClusterComposition( brokenConnection2 ) )
.thenThrow( new ServiceUnavailableException( "Can't connect" ) );
RoutingTable routingTable = new TestRoutingTable( A, B );
try
{
rediscover( B, connections, routingTable, clusterComposition );
fail( "Exception expected" );
}
catch ( Exception e )
{
assertThat( e, instanceOf( ServiceUnavailableException.class ) );
}
verify( clusterComposition ).getClusterComposition( brokenConnection1 );
verify( clusterComposition ).getClusterComposition( brokenConnection2 );
verify( connections ).acquire( A );
verify( connections ).acquire( B );
}
}
private static ClusterComposition rediscover( ConnectionPool connections, RoutingTable routingTable,
ClusterCompositionProvider provider )
{
return rediscover( LOCAL_DEFAULT, connections, routingTable, provider );
}
private static ClusterComposition rediscover( BoltServerAddress initialRouter, ConnectionPool connections,
RoutingTable routingTable, ClusterCompositionProvider provider )
{
RoutingSettings settings = new RoutingSettings( 1, 0, null );
Clock mockedClock = mock( Clock.class );
Logger mockedLogger = mock( Logger.class );
Rediscovery rediscovery = new Rediscovery( initialRouter, settings, mockedClock, mockedLogger, provider,
directMapProvider );
return rediscovery.lookupClusterComposition( routingTable, connections );
}
private static class TestRoutingTable extends ClusterRoutingTable
{
final List<BoltServerAddress> removedRouters = new ArrayList<>();
TestRoutingTable( BoltServerAddress... routers )
{
super( Clock.SYSTEM, routers );
}
@Override
public void forget( BoltServerAddress router )
{
super.forget( router );
removedRouters.add( router );
}
}
}