NameResolverRegistry and better-defined target string.

- NameResolverRegistry contains all the official NameResolvers. Users
  can also add custom NameResolvers to it. It looks up NameResolver by
  try-and-fail. It is the default NameResolver.Factory for builders.
  DnsNameResolver.
- Pass target as Strings instead of URIs from the channel builder to
  ManagedChannelImpl. A target string is not necessarily a valid URI, in
  which case ManagedChannelImpl will add "dns:///" to the beginning of
  the target and use it as URI.
- DnsNameResolver will require scheme "dns" to be present. It no longer
  allows scheme-absent URIs.
This commit is contained in:
Kun Zhang 2015-10-22 14:59:44 -07:00
parent 6dc5e8075b
commit efac679abc
9 changed files with 185 additions and 35 deletions

View File

@ -47,7 +47,16 @@ import javax.annotation.Nullable;
/**
* A factory for DNS-based {@link NameResolver}s.
*
* <p>The format of the target URI is {@code "[dns://[<DNS_server_address>]/]<name>"}.
* <p>It resolves a target URI whose scheme is {@code "dns"}. The (optional) authority of the target
* URI is reserved for the address of alternative DNS server (not implemented yet). The path of the
* target URI, exluding the leading slash {@code '/'}, is treated as the host name to be resolved by
* DNS. Example target URIs:
* <ul>
* <li>{@code "dns:///foo.googleapis.com:8080"} (using default DNS)</li>
* <li>{@code "dns://8.8.8.8/foo.googleapis.com:8080"} (using alternative DNS (not implemented
* yet))</li>
* <li>{@code "dns:///foo.googleapis.com"} (without port)</li>
* </ul>
*/
@ExperimentalApi
public final class DnsNameResolverFactory extends NameResolver.Factory {
@ -56,10 +65,7 @@ public final class DnsNameResolverFactory extends NameResolver.Factory {
@Override
public NameResolver newNameResolver(URI targetUri) {
String scheme = targetUri.getScheme();
if (scheme == null) {
return new DnsNameResolver(null, targetUri.toString());
} else if (scheme.equals("dns")) {
if ("dns".equals(targetUri.getScheme())) {
String targetPath = Preconditions.checkNotNull(targetUri.getPath(), "targetPath");
Preconditions.checkArgument(targetPath.startsWith("/"),
"the path component (%s) of the target (%s) must start with '/'", targetPath, targetUri);

View File

@ -44,6 +44,31 @@ public abstract class ManagedChannelBuilder<T extends ManagedChannelBuilder<T>>
return ManagedChannelProvider.provider().builderForAddress(name, port);
}
/**
* Creates a channel with a target string, which can be either a valid {@link
* NameResolver}-compliant URI, or a HOST:PORT string.
*
* <p>Example {@code NameResolver}-compliant URIs:
* <ul>
* <li>{@code "dns:///foo.googleapis.com:8080"}</li>
* <li>{@code "dns:///foo.googleapis.com"}</li>
* <li>{@code "dns://8.8.8.8/foo.googleapis.com:8080"}</li>
* <li>{@code "dns://8.8.8.8/foo.googleapis.com"}</li>
* <li>{@code "zookeeper://zk.example.com:9900/example_service"}</li>
* </ul>
*
* <p>Example HOST:PORT strings, which will be converted to {@code NameResolver}-compliant URIs by
* prepending {@code "dns:///"}.
* <ul>
* <li>{@code "localhost"}</li>
* <li>{@code "127.0.0.1"}</li>
* <li>{@code "localhost:8080"}</li>
* <li>{@code "foo.googleapis.com:8080"}</li>
* <li>{@code "127.0.0.1:8080"}</li>
* <li>{@code "[2001:db8:85a3:8d3:1319:8a2e:370:7348]"}</li>
* <li>{@code "[2001:db8:85a3:8d3:1319:8a2e:370:7348]:443"}</li>
* </ul>
*/
@ExperimentalApi
public static ManagedChannelBuilder<?> forTarget(String target) {
return ManagedChannelProvider.provider().builderForTarget(target);

View File

@ -41,10 +41,12 @@ import javax.annotation.concurrent.ThreadSafe;
* A pluggable component that resolves a target URI (which is broken down into 3 parts as described
* below) and return addresses to the caller.
*
* <p>The format of the target URI is {@code "[<scheme>:]<scheme-specific-string>"}
* <p>The target URI is a {@link URI} instance. A {@code NameResolver} uses the scheme to determine
* whether it can resolve a given URI, and uses the components after the scheme for actual
* resolution.
*
* <p>{@code NameResolver} has no knowledge of load-balancing. The addresses of a target may be
* changed over time, thus the caller registers a {@link Listener} to receive continuous updates.
* <p>The addresses and attributes of a target may be changed over time, thus the caller registers a
* {@link Listener} to receive continuous updates.
*/
@ExperimentalApi
@ThreadSafe
@ -71,7 +73,10 @@ public abstract class NameResolver {
public abstract static class Factory {
/**
* Creates a {@link NameResolver} for the given target URI, or {@code null} if the given URI
* cannot be resolved by this factory.
* cannot be resolved by this factory. The decision should be solely based on the scheme of the
* URI.
*
* @param targetUri the target URI to be resolved, whose scheme must not be {@code null}
*/
@Nullable
public abstract NameResolver newNameResolver(URI targetUri);

View File

@ -0,0 +1,86 @@
/*
* Copyright 2015, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc;
import java.net.URI;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.annotation.concurrent.ThreadSafe;
/**
* A registry that holds various {@link NameResolver.Factory}s and dispatches target URI to the
* first one that can handle it.
*/
@ExperimentalApi
@ThreadSafe
public final class NameResolverRegistry extends NameResolver.Factory {
private static final NameResolverRegistry defaultRegistry = new NameResolverRegistry();
private final CopyOnWriteArrayList<NameResolver.Factory> registry =
new CopyOnWriteArrayList<NameResolver.Factory>();
private NameResolverRegistry() {
// To prevent instantiation
}
static {
defaultRegistry.register(DnsNameResolverFactory.getInstance());
}
public static NameResolverRegistry getDefaultRegistry() {
return defaultRegistry;
}
/**
* Registers a {@link NameResolver.Factory}.
*/
public void register(NameResolver.Factory factory) {
registry.add(0, factory);
}
/**
* Returns a {@link NameResolver} created by the first factory that can handle the given target
* URI, or {@code null} if no one can handle it.
*
* <p>The factory that was registered later has higher priority.
*/
@Override
public NameResolver newNameResolver(URI targetUri) {
for (NameResolver.Factory factory : registry) {
NameResolver resolver = factory.newNameResolver(targetUri);
if (resolver != null) {
return resolver;
}
}
return null;
}
}

View File

@ -35,11 +35,11 @@ import com.google.common.base.Preconditions;
import io.grpc.Attributes;
import io.grpc.ClientInterceptor;
import io.grpc.DnsNameResolverFactory;
import io.grpc.Internal;
import io.grpc.LoadBalancer;
import io.grpc.ManagedChannelBuilder;
import io.grpc.NameResolver;
import io.grpc.NameResolverRegistry;
import io.grpc.ResolvedServerInfo;
import io.grpc.SimpleLoadBalancerFactory;
@ -65,7 +65,7 @@ public abstract class AbstractManagedChannelImplBuilder
private Executor executor;
private final List<ClientInterceptor> interceptors = new ArrayList<ClientInterceptor>();
private final URI target;
private final String target;
@Nullable
private final SocketAddress directServerAddress;
@ -82,13 +82,13 @@ public abstract class AbstractManagedChannelImplBuilder
@Nullable
private LoadBalancer.Factory loadBalancerFactory;
protected AbstractManagedChannelImplBuilder(URI target) {
protected AbstractManagedChannelImplBuilder(String target) {
this.target = Preconditions.checkNotNull(target);
this.directServerAddress = null;
}
protected AbstractManagedChannelImplBuilder(SocketAddress directServerAddress, String authority) {
this.target = URI.create("direct-address:///" + directServerAddress);
this.target = "directaddress:///" + directServerAddress;
this.directServerAddress = directServerAddress;
this.nameResolverFactory = new DirectAddressNameResolverFactory(directServerAddress, authority);
}
@ -163,9 +163,10 @@ public abstract class AbstractManagedChannelImplBuilder
target,
// TODO(carl-mastrangelo): Allow clients to pass this in
new ExponentialBackoffPolicy.Provider(),
// TODO(zhangkun83): use a NameResolver registry for the "nameResolverFactory == null" case
nameResolverFactory == null ? DnsNameResolverFactory.getInstance() : nameResolverFactory,
loadBalancerFactory == null ? SimpleLoadBalancerFactory.getInstance() : loadBalancerFactory,
nameResolverFactory == null ? NameResolverRegistry.getDefaultRegistry()
: nameResolverFactory,
loadBalancerFactory == null ? SimpleLoadBalancerFactory.getInstance()
: loadBalancerFactory,
transportFactory, executor, userAgent, interceptors);
}

View File

@ -58,6 +58,7 @@ import io.grpc.internal.ClientCallImpl.ClientTransportProvider;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -131,7 +132,7 @@ public final class ManagedChannelImpl extends ManagedChannel {
}
};
ManagedChannelImpl(URI targetUri, BackoffPolicy.Provider backoffPolicyProvider,
ManagedChannelImpl(String target, BackoffPolicy.Provider backoffPolicyProvider,
NameResolver.Factory nameResolverFactory, LoadBalancer.Factory loadBalancerFactory,
ClientTransportFactory transportFactory, @Nullable Executor executor,
@Nullable String userAgent, List<ClientInterceptor> interceptors) {
@ -143,9 +144,38 @@ public final class ManagedChannelImpl extends ManagedChannel {
this.executor = executor;
}
this.backoffPolicyProvider = backoffPolicyProvider;
this.nameResolver = nameResolverFactory.newNameResolver(targetUri);
Preconditions.checkArgument(this.nameResolver != null,
"The given NameResolverFactory cannot resolve %s", targetUri);
// Finding a NameResolver. Try using the target string as the URI. If that fails, try prepending
// "dns:///".
NameResolver nameResolver = null;
URI targetUri;
StringBuilder uriSyntaxErrors = new StringBuilder();
try {
targetUri = new URI(target);
nameResolver = nameResolverFactory.newNameResolver(targetUri);
// For "localhost:8080" this would likely return null, because "localhost" is parsed as the
// scheme. Will fall into the next branch and try "dns:///localhost:8080".
} catch (URISyntaxException e) {
// "foo.googleapis.com:8080" will trigger this exception, because "foo.googleapis.com" is an
// invalid scheme. Just fall through and will try "dns:///foo.googleapis.com:8080"
uriSyntaxErrors.append(e.getMessage());
}
if (nameResolver == null) {
try {
targetUri = new URI("dns:///" + target);
nameResolver = nameResolverFactory.newNameResolver(targetUri);
} catch (URISyntaxException e) {
if (uriSyntaxErrors.length() > 0) {
uriSyntaxErrors.append("; ");
}
uriSyntaxErrors.append(e.getMessage());
}
}
Preconditions.checkArgument(nameResolver != null,
"cannot find a NameResolver for %s%s", target,
uriSyntaxErrors.length() > 0 ? " (" + uriSyntaxErrors.toString() + ")" : "");
this.nameResolver = nameResolver;
this.loadBalancer = loadBalancerFactory.newLoadBalancer(nameResolver.getServiceAuthority(), tm);
this.transportFactory = transportFactory;
this.userAgent = userAgent;

View File

@ -94,8 +94,8 @@ public class ManagedChannelImplTest {
new StringMarshaller(), new IntegerMarshaller());
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final String serviceName = "fake.example.com";
private final URI target = URI.create("//" + serviceName);
private final String authority = serviceName;
private final String target = "fake://" + serviceName;
private final SocketAddress socketAddress = new SocketAddress() {};
private final ResolvedServerInfo server = new ResolvedServerInfo(socketAddress, Attributes.EMPTY);
@ -328,12 +328,11 @@ public class ManagedChannelImplTest {
@Override
public NameResolver newNameResolver(final URI targetUri) {
assertEquals(null, targetUri.getScheme());
assertEquals("fake", targetUri.getScheme());
assertEquals(serviceName, targetUri.getAuthority());
return new NameResolver() {
@Override public String getServiceAuthority() {
assertNotNull(targetUri.toString() + " has authority", targetUri.getAuthority());
return targetUri.getAuthority();
return serviceName;
}
@Override public void start(final Listener listener) {

View File

@ -51,7 +51,6 @@ import io.netty.handler.ssl.SslContext;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import javax.annotation.Nullable;
import javax.net.ssl.SSLException;
@ -89,14 +88,14 @@ public class NettyChannelBuilder extends AbstractManagedChannelImplBuilder<Netty
}
/**
* Creates a new builder with the given target URI that will be resolved by
* Creates a new builder with the given target string that will be resolved by
* {@link io.grpc.NameResolver}.
*/
public static NettyChannelBuilder forTarget(String targetUri) {
return new NettyChannelBuilder(URI.create(targetUri));
public static NettyChannelBuilder forTarget(String target) {
return new NettyChannelBuilder(target);
}
private NettyChannelBuilder(URI target) {
private NettyChannelBuilder(String target) {
super(target);
}

View File

@ -52,7 +52,6 @@ import io.grpc.internal.SharedResourceHolder.Resource;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -103,11 +102,11 @@ public class OkHttpChannelBuilder extends
}
/**
* Creates a new builder for the given target URI that will be resolved by
* Creates a new builder for the given target that will be resolved by
* {@link io.grpc.NameResolver}.
*/
public static OkHttpChannelBuilder forTarget(String targetUri) {
return new OkHttpChannelBuilder(URI.create(targetUri));
public static OkHttpChannelBuilder forTarget(String target) {
return new OkHttpChannelBuilder(target);
}
private Executor transportExecutor;
@ -118,10 +117,10 @@ public class OkHttpChannelBuilder extends
private int maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE;
protected OkHttpChannelBuilder(String host, int port) {
this(URI.create("dns:///" + GrpcUtil.authorityFromHostAndPort(host, port)));
this(GrpcUtil.authorityFromHostAndPort(host, port));
}
private OkHttpChannelBuilder(URI target) {
private OkHttpChannelBuilder(String target) {
super(target);
}