diff --git a/driver/src/main/java/org/neo4j/driver/internal/BoltServerAddress.java b/driver/src/main/java/org/neo4j/driver/internal/BoltServerAddress.java index 066afa2aa2..b6b85d9749 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/BoltServerAddress.java +++ b/driver/src/main/java/org/neo4j/driver/internal/BoltServerAddress.java @@ -24,12 +24,14 @@ import java.net.URI; import java.net.UnknownHostException; +import org.neo4j.driver.v1.net.ServerAddress; + import static java.util.Objects.requireNonNull; /** * Holds a host and port pair that denotes a Bolt server address. */ -public class BoltServerAddress +public class BoltServerAddress implements ServerAddress { public static final int DEFAULT_PORT = 7687; public static final BoltServerAddress LOCAL_DEFAULT = new BoltServerAddress( "localhost", DEFAULT_PORT ); @@ -50,11 +52,18 @@ public BoltServerAddress( URI uri ) public BoltServerAddress( String host, int port ) { - this.host = requireNonNull( host ); - this.port = port; + this.host = requireNonNull( host, "host" ); + this.port = requireValidPort( port ); this.stringValue = String.format( "%s:%d", host, port ); } + public static BoltServerAddress from( ServerAddress address ) + { + return address instanceof BoltServerAddress + ? (BoltServerAddress) address + : new BoltServerAddress( address.host(), address.port() ); + } + @Override public boolean equals( Object o ) { @@ -114,11 +123,13 @@ public BoltServerAddress resolve() throws UnknownHostException } } + @Override public String host() { return host; } + @Override public int port() { return port; @@ -194,4 +205,13 @@ private static RuntimeException invalidAddressFormat( String address ) { return new IllegalArgumentException( "Invalid address format `" + address + "`" ); } + + private static int requireValidPort( int port ) + { + if ( port >= 0 && port <= 65_535 ) + { + return port; + } + throw new IllegalArgumentException( "Illegal port: " + port ); + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java index 9a05a2132d..1079634017 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java +++ b/driver/src/main/java/org/neo4j/driver/internal/DriverFactory.java @@ -31,6 +31,7 @@ import org.neo4j.driver.internal.async.ChannelConnectorImpl; import org.neo4j.driver.internal.async.pool.ConnectionPoolImpl; import org.neo4j.driver.internal.async.pool.PoolSettings; +import org.neo4j.driver.internal.cluster.DnsResolver; import org.neo4j.driver.internal.cluster.RoutingContext; import org.neo4j.driver.internal.cluster.RoutingSettings; import org.neo4j.driver.internal.cluster.loadbalancing.LeastConnectedLoadBalancingStrategy; @@ -58,6 +59,7 @@ import org.neo4j.driver.v1.Logging; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.net.ServerAddressResolver; import static java.lang.String.format; import static org.neo4j.driver.internal.metrics.InternalAbstractMetrics.DEV_NULL_METRICS; @@ -204,8 +206,9 @@ protected LoadBalancer createLoadBalancer( BoltServerAddress address, Connection EventExecutorGroup eventExecutorGroup, Config config, RoutingSettings routingSettings ) { LoadBalancingStrategy loadBalancingStrategy = createLoadBalancingStrategy( config, connectionPool ); + ServerAddressResolver resolver = createResolver( config ); return new LoadBalancer( address, routingSettings, connectionPool, eventExecutorGroup, createClock(), - config.logging(), loadBalancingStrategy ); + config.logging(), loadBalancingStrategy, resolver ); } private static LoadBalancingStrategy createLoadBalancingStrategy( Config config, @@ -222,6 +225,12 @@ private static LoadBalancingStrategy createLoadBalancingStrategy( Config config, } } + private static ServerAddressResolver createResolver( Config config ) + { + ServerAddressResolver configuredResolver = config.resolver(); + return configuredResolver != null ? configuredResolver : new DnsResolver( config.logging() ); + } + /** * Creates new {@link Clock}. *

diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/DnsResolver.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/DnsResolver.java index c3638e97e9..880ad56ffd 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/DnsResolver.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/DnsResolver.java @@ -20,42 +20,40 @@ import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.HashSet; import java.util.Set; +import java.util.stream.Stream; import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.net.ServerAddress; +import org.neo4j.driver.v1.net.ServerAddressResolver; -public class DnsResolver implements HostNameResolver +import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toSet; + +public class DnsResolver implements ServerAddressResolver { private final Logger logger; - public DnsResolver( Logger logger ) + public DnsResolver( Logging logging ) { - this.logger = logger; + this.logger = logging.getLog( DnsResolver.class.getSimpleName() ); } @Override - public Set resolve( BoltServerAddress initialRouter ) + public Set resolve( ServerAddress initialRouter ) { - Set addresses = new HashSet<>(); try { - InetAddress[] ipAddresses = InetAddress.getAllByName( initialRouter.host() ); - - for ( InetAddress ipAddress : ipAddresses ) - { - addresses.add( new BoltServerAddress( ipAddress.getHostAddress(), initialRouter.port() ) ); - } - - return addresses; + return Stream.of( InetAddress.getAllByName( initialRouter.host() ) ) + .map( address -> new BoltServerAddress( address.getHostAddress(), initialRouter.port() ) ) + .collect( toSet() ); } catch ( UnknownHostException e ) { - logger.error( "Failed to resolve URI `" + initialRouter + "` to IPs due to error: " + e.getMessage(), e ); - - addresses.add( initialRouter ); - return addresses; + logger.error( "Failed to resolve address `" + initialRouter + "` to IPs due to error: " + e.getMessage(), e ); + return singleton( initialRouter ); } } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java index 0e854622ca..56db7d6a88 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java @@ -20,8 +20,8 @@ import io.netty.util.concurrent.EventExecutorGroup; -import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -35,9 +35,13 @@ import org.neo4j.driver.v1.Logger; import org.neo4j.driver.v1.exceptions.SecurityException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.net.ServerAddressResolver; import static java.lang.String.format; +import static java.util.Collections.emptySet; +import static java.util.Collections.singletonList; import static java.util.concurrent.CompletableFuture.completedFuture; +import static java.util.stream.Collectors.toList; import static org.neo4j.driver.internal.util.Futures.completedWithNull; public class Rediscovery @@ -48,27 +52,26 @@ public class Rediscovery private final RoutingSettings settings; private final Logger logger; private final ClusterCompositionProvider provider; - private final HostNameResolver hostNameResolver; + private final ServerAddressResolver resolver; private final EventExecutorGroup eventExecutorGroup; private volatile boolean useInitialRouter; public Rediscovery( BoltServerAddress initialRouter, RoutingSettings settings, ClusterCompositionProvider provider, - EventExecutorGroup eventExecutorGroup, HostNameResolver hostNameResolver, Logger logger ) + EventExecutorGroup eventExecutorGroup, ServerAddressResolver resolver, Logger logger ) { - this( initialRouter, settings, provider, hostNameResolver, eventExecutorGroup, logger, true ); + this( initialRouter, settings, provider, resolver, eventExecutorGroup, logger, true ); } // Test-only constructor - public Rediscovery( BoltServerAddress initialRouter, RoutingSettings settings, ClusterCompositionProvider provider, - HostNameResolver hostNameResolver, EventExecutorGroup eventExecutorGroup, Logger logger, - boolean useInitialRouter ) + Rediscovery( BoltServerAddress initialRouter, RoutingSettings settings, ClusterCompositionProvider provider, + ServerAddressResolver resolver, EventExecutorGroup eventExecutorGroup, Logger logger, boolean useInitialRouter ) { this.initialRouter = initialRouter; this.settings = settings; this.logger = logger; this.provider = provider; - this.hostNameResolver = hostNameResolver; + this.resolver = resolver; this.eventExecutorGroup = eventExecutorGroup; this.useInitialRouter = useInitialRouter; } @@ -163,7 +166,7 @@ private CompletionStage lookupOnKnownRoutersThenOnInitialRou private CompletionStage lookupOnInitialRouterThenOnKnownRouters( RoutingTable routingTable, ConnectionPool connectionPool ) { - Set seenServers = Collections.emptySet(); + Set seenServers = emptySet(); return lookupOnInitialRouter( routingTable, connectionPool, seenServers ).thenCompose( composition -> { if ( composition != null ) @@ -201,7 +204,7 @@ private CompletionStage lookupOnKnownRouters( RoutingTable r private CompletionStage lookupOnInitialRouter( RoutingTable routingTable, ConnectionPool connectionPool, Set seenServers ) { - Set addresses = hostNameResolver.resolve( initialRouter ); + List addresses = resolve( initialRouter ); addresses.removeAll( seenServers ); CompletableFuture result = completedWithNull(); @@ -255,4 +258,19 @@ private ClusterComposition handleRoutingProcedureError( Throwable error, Routing } } + private List resolve( BoltServerAddress address ) + { + try + { + return resolver.resolve( address ) + .stream() + .map( BoltServerAddress::from ) + .collect( toList() ); // collect to list to preserve the order + } + catch ( Throwable error ) + { + logger.error( "Resolver function failed to resolve '" + address + "'. The address will be used as is", error ); + return singletonList( address ); + } + } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java index a60e3546a3..4ceb337255 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/loadbalancing/LoadBalancer.java @@ -30,7 +30,6 @@ import org.neo4j.driver.internal.cluster.ClusterComposition; import org.neo4j.driver.internal.cluster.ClusterCompositionProvider; import org.neo4j.driver.internal.cluster.ClusterRoutingTable; -import org.neo4j.driver.internal.cluster.DnsResolver; import org.neo4j.driver.internal.cluster.Rediscovery; import org.neo4j.driver.internal.cluster.RoutingProcedureClusterCompositionProvider; import org.neo4j.driver.internal.cluster.RoutingSettings; @@ -45,6 +44,7 @@ import org.neo4j.driver.v1.Logging; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.SessionExpiredException; +import org.neo4j.driver.v1.net.ServerAddressResolver; import static java.util.concurrent.CompletableFuture.completedFuture; @@ -63,15 +63,15 @@ public class LoadBalancer implements ConnectionProvider, RoutingErrorHandler public LoadBalancer( BoltServerAddress initialRouter, RoutingSettings settings, ConnectionPool connectionPool, EventExecutorGroup eventExecutorGroup, Clock clock, Logging logging, - LoadBalancingStrategy loadBalancingStrategy ) + LoadBalancingStrategy loadBalancingStrategy, ServerAddressResolver resolver ) { this( connectionPool, new ClusterRoutingTable( clock, initialRouter ), - createRediscovery( initialRouter, settings, eventExecutorGroup, clock, logging ), + createRediscovery( initialRouter, settings, eventExecutorGroup, resolver, clock, logging ), loadBalancerLogger( logging ), loadBalancingStrategy, eventExecutorGroup ); } // Used only in testing - public LoadBalancer( ConnectionPool connectionPool, RoutingTable routingTable, Rediscovery rediscovery, + LoadBalancer( ConnectionPool connectionPool, RoutingTable routingTable, Rediscovery rediscovery, EventExecutorGroup eventExecutorGroup, Logging logging ) { this( connectionPool, routingTable, rediscovery, loadBalancerLogger( logging ), @@ -264,13 +264,11 @@ private BoltServerAddress selectAddress( AccessMode mode, AddressSet servers ) } private static Rediscovery createRediscovery( BoltServerAddress initialRouter, RoutingSettings settings, - EventExecutorGroup eventExecutorGroup, Clock clock, Logging logging ) + EventExecutorGroup eventExecutorGroup, ServerAddressResolver resolver, Clock clock, Logging logging ) { Logger log = loadBalancerLogger( logging ); - ClusterCompositionProvider clusterCompositionProvider = - new RoutingProcedureClusterCompositionProvider( clock, settings ); - return new Rediscovery( initialRouter, settings, clusterCompositionProvider, eventExecutorGroup, - new DnsResolver( log ), log ); + ClusterCompositionProvider clusterCompositionProvider = new RoutingProcedureClusterCompositionProvider( clock, settings ); + return new Rediscovery( initialRouter, settings, clusterCompositionProvider, eventExecutorGroup, resolver, log ); } private static Logger loadBalancerLogger( Logging logging ) diff --git a/driver/src/main/java/org/neo4j/driver/v1/Config.java b/driver/src/main/java/org/neo4j/driver/v1/Config.java index 3224cc4a1b..50073aef19 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/Config.java +++ b/driver/src/main/java/org/neo4j/driver/v1/Config.java @@ -19,6 +19,8 @@ package org.neo4j.driver.v1; import java.io.File; +import java.net.InetAddress; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -28,6 +30,7 @@ import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.SessionExpiredException; import org.neo4j.driver.v1.exceptions.TransientException; +import org.neo4j.driver.v1.net.ServerAddressResolver; import org.neo4j.driver.v1.util.Experimental; import org.neo4j.driver.v1.util.Immutable; import org.neo4j.driver.v1.util.Resource; @@ -88,6 +91,7 @@ public class Config private final RetrySettings retrySettings; private final LoadBalancingStrategy loadBalancingStrategy; + private final ServerAddressResolver resolver; private Config( ConfigBuilder builder) { @@ -106,6 +110,7 @@ private Config( ConfigBuilder builder) this.connectionTimeoutMillis = builder.connectionTimeoutMillis; this.retrySettings = builder.retrySettings; this.loadBalancingStrategy = builder.loadBalancingStrategy; + this.resolver = builder.resolver; } /** @@ -216,9 +221,9 @@ public TrustStrategy trustStrategy() } /** - * Load balancing strategy + * Load balancing strategy. * - * @return the strategy to use + * @return the strategy to use. */ @Experimental public LoadBalancingStrategy loadBalancingStrategy() @@ -226,6 +231,16 @@ public LoadBalancingStrategy loadBalancingStrategy() return loadBalancingStrategy; } + /** + * Server address resolver. + * + * @return the resolver to use. + */ + public ServerAddressResolver resolver() + { + return resolver; + } + /** * Return a {@link ConfigBuilder} instance * @return a {@link ConfigBuilder} instance @@ -271,6 +286,7 @@ public static class ConfigBuilder private long routingRetryDelayMillis = RoutingSettings.DEFAULT.retryTimeoutDelay(); private int connectionTimeoutMillis = (int) TimeUnit.SECONDS.toMillis( 5 ); private RetrySettings retrySettings = RetrySettings.DEFAULT; + private ServerAddressResolver resolver; private ConfigBuilder() {} @@ -711,6 +727,25 @@ public ConfigBuilder withMaxTransactionRetryTime( long value, TimeUnit unit ) return this; } + /** + * Specify a custom server address resolver used by the routing driver to resolve the initial address used to create the driver. + * Such resolution happens: + *

+ * By default driver performs a DNS lookup for the initial address using {@link InetAddress#getAllByName(String)}. + * + * @param resolver the resolver to use. + * @return this builder. + * @throws NullPointerException when the given resolver is {@code null}. + */ + public ConfigBuilder withResolver( ServerAddressResolver resolver ) + { + this.resolver = Objects.requireNonNull( resolver, "resolver" ); + return this; + } + /** * Create a config instance from this builder. * @return a {@link Config} instance diff --git a/driver/src/main/java/org/neo4j/driver/v1/net/ServerAddress.java b/driver/src/main/java/org/neo4j/driver/v1/net/ServerAddress.java new file mode 100644 index 0000000000..9e638fd477 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/v1/net/ServerAddress.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2002-2018 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.v1.net; + +import org.neo4j.driver.internal.BoltServerAddress; + +/** + * Represents a host and port. Host can either be an IP address or a DNS name. + * Both IPv4 and IPv6 hosts are supported. + */ +public interface ServerAddress +{ + /** + * Retrieve the host portion of this {@link ServerAddress}. + * + * @return the host, never {@code null}. + */ + String host(); + + /** + * Retrieve the port portion of this {@link ServerAddress}. + * + * @return the port, always in range [0, 65535]. + */ + int port(); + + /** + * Create a new address with the given host and port. + * + * @param host the host portion. Should not be {@code null}. + * @param port the port portion. Should be in range [0, 65535]. + * @return new server address with the specified host and port. + */ + static ServerAddress of( String host, int port ) + { + return new BoltServerAddress( host, port ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/HostNameResolver.java b/driver/src/main/java/org/neo4j/driver/v1/net/ServerAddressResolver.java similarity index 56% rename from driver/src/main/java/org/neo4j/driver/internal/cluster/HostNameResolver.java rename to driver/src/main/java/org/neo4j/driver/v1/net/ServerAddressResolver.java index d5f575f071..ab5e64433f 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/HostNameResolver.java +++ b/driver/src/main/java/org/neo4j/driver/v1/net/ServerAddressResolver.java @@ -16,13 +16,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.neo4j.driver.internal.cluster; +package org.neo4j.driver.v1.net; import java.util.Set; -import org.neo4j.driver.internal.BoltServerAddress; - -public interface HostNameResolver +/** + * A resolver function used by the routing driver to resolve the initial address used to create the driver. + */ +@FunctionalInterface +public interface ServerAddressResolver { - Set resolve( BoltServerAddress initialRouter ); + /** + * Resolve the given address to a set of other addresses. + * Exceptions thrown by this method will be logged and driver will continue using the original address. + * + * @param address the address to resolve. + * @return new set of addresses. + */ + Set resolve( ServerAddress address ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/DnsResolverTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/DnsResolverTest.java index 224dafcac9..0633227ad4 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/DnsResolverTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/DnsResolverTest.java @@ -25,6 +25,8 @@ import org.neo4j.driver.internal.BoltServerAddress; import org.neo4j.driver.v1.Logger; +import org.neo4j.driver.v1.Logging; +import org.neo4j.driver.v1.net.ServerAddress; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.junit.MatcherAssert.assertThat; @@ -34,33 +36,33 @@ class DnsResolverTest { - private DnsResolver resolver = new DnsResolver( mock( Logger.class ) ); + private DnsResolver resolver = new DnsResolver( Logging.none() ); @Test void shouldResolveDNSToIPs() { - Set resolve = resolver.resolve( new BoltServerAddress( "google.com", 80 ) ); + Set resolve = resolver.resolve( new BoltServerAddress( "google.com", 80 ) ); assertThat( resolve.size(), greaterThanOrEqualTo( 1 ) ); } @Test void shouldResolveLocalhostIPDNSToIPs() { - Set resolve = resolver.resolve( new BoltServerAddress( "127.0.0.1", 80 ) ); + Set resolve = resolver.resolve( new BoltServerAddress( "127.0.0.1", 80 ) ); assertThat( resolve.size(), greaterThanOrEqualTo( 1 ) ); } @Test void shouldResolveLocalhostDNSToIPs() { - Set resolve = resolver.resolve( new BoltServerAddress( "localhost", 80 ) ); + Set resolve = resolver.resolve( new BoltServerAddress( "localhost", 80 ) ); assertThat( resolve.size(), greaterThanOrEqualTo( 1 ) ); } @Test void shouldResolveIPv6LocalhostDNSToIPs() { - Set resolve = resolver.resolve( new BoltServerAddress( "[::1]", 80 ) ); + Set resolve = resolver.resolve( new BoltServerAddress( "[::1]", 80 ) ); assertThat( resolve.size(), greaterThanOrEqualTo( 1 ) ); } @@ -68,8 +70,8 @@ void shouldResolveIPv6LocalhostDNSToIPs() void shouldExceptionAndGiveDefaultValue() { Logger logger = mock( Logger.class ); - DnsResolver resolver = new DnsResolver( logger ); - Set resolve = resolver.resolve( new BoltServerAddress( "[/]", 80 ) ); + DnsResolver resolver = new DnsResolver( name -> logger ); + Set resolve = resolver.resolve( new BoltServerAddress( "[/]", 80 ) ); verify( logger ).error( any( String.class ), any( UnknownHostException.class ) ); assertThat( resolve.size(), greaterThanOrEqualTo( 1 ) ); } diff --git a/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java b/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java index c15625dce5..95aee9a3d4 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/cluster/RediscoveryTest.java @@ -37,6 +37,7 @@ import org.neo4j.driver.v1.exceptions.ProtocolException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.SessionExpiredException; +import org.neo4j.driver.v1.net.ServerAddressResolver; import static java.util.Arrays.asList; import static java.util.Collections.emptySet; @@ -76,7 +77,7 @@ void shouldUseFirstRouterInTable() responsesByAddress.put( B, new Success( expectedComposition ) ); // first -> valid cluster composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( HostNameResolver.class ) ); + Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) ); RoutingTable table = routingTableMock( B ); ClusterComposition actualComposition = await( rediscovery.lookupClusterComposition( table, pool ) ); @@ -97,7 +98,7 @@ void shouldSkipFailingRouters() responsesByAddress.put( C, new Success( expectedComposition ) ); // third -> valid cluster composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( HostNameResolver.class ) ); + Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) ); RoutingTable table = routingTableMock( A, B, C ); ClusterComposition actualComposition = await( rediscovery.lookupClusterComposition( table, pool ) ); @@ -119,7 +120,7 @@ void shouldFailImmediatelyOnAuthError() responsesByAddress.put( B, authError ); // second router -> fatal auth error ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( HostNameResolver.class ) ); + Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) ); RoutingTable table = routingTableMock( A, B, C ); AuthenticationException error = assertThrows( AuthenticationException.class, () -> await( rediscovery.lookupClusterComposition( table, pool ) ) ); @@ -140,7 +141,7 @@ void shouldFallbackToInitialRouterWhenKnownRoutersFail() responsesByAddress.put( initialRouter, new Success( expectedComposition ) ); // initial -> valid response ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - HostNameResolver resolver = hostNameResolverMock( initialRouter, initialRouter ); + ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver ); RoutingTable table = routingTableMock( B, C ); @@ -163,7 +164,7 @@ void shouldFailImmediatelyWhenClusterCompositionProviderReturnsFailure() responsesByAddress.put( C, new Success( validComposition ) ); // second -> valid cluster composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( HostNameResolver.class ) ); + Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) ); RoutingTable table = routingTableMock( B, C ); ProtocolException error = assertThrows( ProtocolException.class, () -> await( rediscovery.lookupClusterComposition( table, pool ) ) ); @@ -185,7 +186,7 @@ void shouldResolveInitialRouterAddress() ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); // initial router resolved to two other addresses - HostNameResolver resolver = hostNameResolverMock( initialRouter, D, E ); + ServerAddressResolver resolver = resolverMock( initialRouter, D, E ); Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver ); RoutingTable table = routingTableMock( B, C ); @@ -197,6 +198,57 @@ void shouldResolveInitialRouterAddress() verify( table ).forget( D ); } + @Test + void shouldResolveInitialRouterAddressUsingCustomResolver() + { + ClusterComposition expectedComposition = new ClusterComposition( 42, + asOrderedSet( A, B, C ), asOrderedSet( A, B, C ), asOrderedSet( B, E ) ); + + ServerAddressResolver resolver = address -> + { + assertEquals( A, address ); + return asOrderedSet( B, C, E ); + }; + + Map responsesByAddress = new HashMap<>(); + responsesByAddress.put( B, new ServiceUnavailableException( "Hi!" ) ); // first -> non-fatal failure + responsesByAddress.put( C, new ServiceUnavailableException( "Hi!" ) ); // second -> non-fatal failure + responsesByAddress.put( E, new Success( expectedComposition ) ); // resolved second -> valid response + + ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); + Rediscovery rediscovery = newRediscovery( A, compositionProvider, resolver ); + RoutingTable table = routingTableMock( B, C ); + + ClusterComposition actualComposition = await( rediscovery.lookupClusterComposition( table, pool ) ); + + assertEquals( expectedComposition, actualComposition ); + verify( table ).forget( B ); + verify( table ).forget( C ); + } + + @Test + void shouldUseInitialRouterAddressAsIsWhenResolverFails() + { + ClusterComposition expectedComposition = new ClusterComposition( 42, + asOrderedSet( A, B ), asOrderedSet( A, B ), asOrderedSet( A, B ) ); + + Map responsesByAddress = singletonMap( A, new Success( expectedComposition ) ); + ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); + + // failing server address resolver + ServerAddressResolver resolver = mock( ServerAddressResolver.class ); + when( resolver.resolve( A ) ).thenThrow( new RuntimeException( "Resolver fails!" ) ); + + Rediscovery rediscovery = newRediscovery( A, compositionProvider, resolver ); + RoutingTable table = routingTableMock(); + + ClusterComposition actualComposition = await( rediscovery.lookupClusterComposition( table, pool ) ); + + assertEquals( expectedComposition, actualComposition ); + verify( resolver ).resolve( A ); + verify( table, never() ).forget( any() ); + } + @Test void shouldFailWhenNoRoutersRespond() { @@ -206,7 +258,7 @@ void shouldFailWhenNoRoutersRespond() responsesByAddress.put( C, new IOException( "Hi!" ) ); // third -> non-fatal failure ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( HostNameResolver.class ) ); + Rediscovery rediscovery = newRediscovery( A, compositionProvider, mock( ServerAddressResolver.class ) ); RoutingTable table = routingTableMock( A, B, C ); ServiceUnavailableException e = assertThrows( ServiceUnavailableException.class, () -> await( rediscovery.lookupClusterComposition( table, pool ) ) ); @@ -227,7 +279,7 @@ void shouldUseInitialRouterAfterDiscoveryReturnsNoWriters() responsesByAddress.put( initialRouter, new Success( validComposition ) ); // initial -> valid composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - HostNameResolver resolver = hostNameResolverMock( initialRouter, initialRouter ); + ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver ); RoutingTable table = routingTableMock( B ); @@ -249,7 +301,7 @@ void shouldUseInitialRouterToStartWith() responsesByAddress.put( initialRouter, new Success( validComposition ) ); // initial -> valid composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - HostNameResolver resolver = hostNameResolverMock( initialRouter, initialRouter ); + ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver, true ); RoutingTable table = routingTableMock( B, C, D ); @@ -270,7 +322,7 @@ void shouldUseKnownRoutersWhenInitialRouterFails() responsesByAddress.put( E, new Success( validComposition ) ); // second known -> valid composition ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - HostNameResolver resolver = hostNameResolverMock( initialRouter, initialRouter ); + ServerAddressResolver resolver = resolverMock( initialRouter, initialRouter ); Rediscovery rediscovery = newRediscovery( initialRouter, compositionProvider, resolver, true ); RoutingTable table = routingTableMock( D, E ); @@ -294,7 +346,7 @@ void shouldRetryConfiguredNumberOfTimesWithDelay() responsesByAddress.put( E, new Success( expectedComposition ) ); ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - HostNameResolver resolver = mock( HostNameResolver.class ); + ServerAddressResolver resolver = mock( ServerAddressResolver.class ); when( resolver.resolve( A ) ).thenReturn( asOrderedSet( A ) ) .thenReturn( asOrderedSet( A ) ) .thenReturn( asOrderedSet( E ) ); @@ -321,7 +373,7 @@ void shouldNotLogWhenSingleRetryAttemptFails() Map responsesByAddress = singletonMap( A, new ServiceUnavailableException( "Hi!" ) ); ClusterCompositionProvider compositionProvider = compositionProviderMock( responsesByAddress ); - HostNameResolver resolver = hostNameResolverMock( A, A ); + ServerAddressResolver resolver = resolverMock( A, A ); ImmediateSchedulingEventExecutor eventExecutor = new ImmediateSchedulingEventExecutor(); RoutingSettings settings = new RoutingSettings( maxRoutingFailures, retryTimeoutDelay ); @@ -339,16 +391,16 @@ void shouldNotLogWhenSingleRetryAttemptFails() } private Rediscovery newRediscovery( BoltServerAddress initialRouter, ClusterCompositionProvider compositionProvider, - HostNameResolver hostNameResolver ) + ServerAddressResolver resolver ) { - return newRediscovery( initialRouter, compositionProvider, hostNameResolver, false ); + return newRediscovery( initialRouter, compositionProvider, resolver, false ); } private Rediscovery newRediscovery( BoltServerAddress initialRouter, ClusterCompositionProvider compositionProvider, - HostNameResolver hostNameResolver, boolean useInitialRouter ) + ServerAddressResolver resolver, boolean useInitialRouter ) { RoutingSettings settings = new RoutingSettings( 1, 0 ); - return new Rediscovery( initialRouter, settings, compositionProvider, hostNameResolver, + return new Rediscovery( initialRouter, settings, compositionProvider, resolver, GlobalEventExecutor.INSTANCE, DEV_NULL_LOGGER, useInitialRouter ); } @@ -375,9 +427,9 @@ private static ClusterCompositionProvider compositionProviderMock( return provider; } - private static HostNameResolver hostNameResolverMock( BoltServerAddress address, BoltServerAddress... resolved ) + private static ServerAddressResolver resolverMock( BoltServerAddress address, BoltServerAddress... resolved ) { - HostNameResolver resolver = mock( HostNameResolver.class ); + ServerAddressResolver resolver = mock( ServerAddressResolver.class ); when( resolver.resolve( address ) ).thenReturn( asOrderedSet( resolved ) ); return resolver; } diff --git a/driver/src/test/java/org/neo4j/driver/internal/net/BoltServerAddressTest.java b/driver/src/test/java/org/neo4j/driver/internal/net/BoltServerAddressTest.java index 0f5f8d4483..b3200a3b75 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/net/BoltServerAddressTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/net/BoltServerAddressTest.java @@ -23,11 +23,16 @@ import java.net.SocketAddress; import org.neo4j.driver.internal.BoltServerAddress; +import org.neo4j.driver.v1.net.ServerAddress; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.junit.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import static org.neo4j.driver.internal.BoltServerAddress.DEFAULT_PORT; class BoltServerAddressTest @@ -61,4 +66,53 @@ void shouldHaveCorrectToString() assertEquals( "localhost:4242", new BoltServerAddress( "localhost", 4242 ).toString() ); assertEquals( "127.0.0.1:8888", new BoltServerAddress( "127.0.0.1", 8888 ).toString() ); } + + @Test + void shouldVerifyHost() + { + assertThrows( NullPointerException.class, () -> new BoltServerAddress( null, 0 ) ); + } + + @Test + void shouldVerifyPort() + { + assertThrows( IllegalArgumentException.class, () -> new BoltServerAddress( "localhost", -1 ) ); + assertThrows( IllegalArgumentException.class, () -> new BoltServerAddress( "localhost", -42 ) ); + assertThrows( IllegalArgumentException.class, () -> new BoltServerAddress( "localhost", 65_536 ) ); + assertThrows( IllegalArgumentException.class, () -> new BoltServerAddress( "localhost", 99_999 ) ); + } + + @Test + void shouldCreateBoltServerAddressFromServerAddress() + { + BoltServerAddress address1 = new BoltServerAddress( "my.server.com", 8899 ); + assertSame( address1, BoltServerAddress.from( address1 ) ); + + BoltServerAddress address2 = new BoltServerAddress( "db.neo4j.com" ); + assertSame( address2, BoltServerAddress.from( address2 ) ); + + ServerAddress address3 = mock( ServerAddress.class ); + when( address3.host() ).thenReturn( "graph.database.com" ); + when( address3.port() ).thenReturn( 20600 ); + assertEquals( new BoltServerAddress( "graph.database.com", 20600 ), BoltServerAddress.from( address3 ) ); + } + + @Test + void shouldFailToCreateBoltServerAddressFromInvalidServerAddress() + { + ServerAddress address1 = mock( ServerAddress.class ); + when( address1.host() ).thenReturn( null ); + when( address1.port() ).thenReturn( 8888 ); + assertThrows( NullPointerException.class, () -> BoltServerAddress.from( address1 ) ); + + ServerAddress address2 = mock( ServerAddress.class ); + when( address2.host() ).thenReturn( "neo4j.host.com" ); + when( address2.port() ).thenReturn( -1 ); + assertThrows( IllegalArgumentException.class, () -> BoltServerAddress.from( address2 ) ); + + ServerAddress address3 = mock( ServerAddress.class ); + when( address3.host() ).thenReturn( "my.database.org" ); + when( address3.port() ).thenReturn( 99_000 ); + assertThrows( IllegalArgumentException.class, () -> BoltServerAddress.from( address3 ) ); + } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java b/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java index 34ea86588c..2548320a32 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java +++ b/driver/src/test/java/org/neo4j/driver/v1/ConfigTest.java @@ -23,11 +23,14 @@ import java.io.File; import java.util.concurrent.TimeUnit; +import org.neo4j.driver.v1.net.ServerAddressResolver; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; class ConfigTest { @@ -271,4 +274,19 @@ void shouldEnableAndDisableHostnameVerificationOnTrustStrategy() assertSame( trustStrategy, trustStrategy.withoutHostnameVerification() ); assertFalse( trustStrategy.isHostnameVerificationEnabled() ); } + + @Test + void shouldAllowToConfigureResolver() + { + ServerAddressResolver resolver = mock( ServerAddressResolver.class ); + Config config = Config.build().withResolver( resolver ).toConfig(); + + assertEquals( resolver, config.resolver() ); + } + + @Test + void shouldNotAllowNullResolver() + { + assertThrows( NullPointerException.class, () -> Config.build().withResolver( null ) ); + } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/net/ServerAddressTest.java b/driver/src/test/java/org/neo4j/driver/v1/net/ServerAddressTest.java new file mode 100644 index 0000000000..da71e2e166 --- /dev/null +++ b/driver/src/test/java/org/neo4j/driver/v1/net/ServerAddressTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2002-2018 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.v1.net; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class ServerAddressTest +{ + @Test + void shouldCreateAddress() + { + ServerAddress address = ServerAddress.of( "my.database.com", 8897 ); + assertEquals( "my.database.com", address.host() ); + assertEquals( 8897, address.port() ); + } + + @Test + void shouldFailToCreateAddressWithInvalidHost() + { + assertThrows( NullPointerException.class, () -> ServerAddress.of( null, 9999 ) ); + } + + @Test + void shouldFailToCreateAddressWithInvalidPort() + { + assertThrows( IllegalArgumentException.class, () -> ServerAddress.of( "hello.graphs.com", -42 ) ); + assertThrows( IllegalArgumentException.class, () -> ServerAddress.of( "hello.graphs.com", 66_000 ) ); + } +}