nl.topicus.jdbc.shaded.io.grpc.internal.DnsNameResolver Maven / Gradle / Ivy
Show all versions of spanner-jdbc Show documentation
/*
* Copyright 2015 The gRPC Authors
*
* 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 nl.topicus.jdbc.shaded.io.grpc.internal;
import static nl.topicus.jdbc.shaded.com.google.common.base.Preconditions.checkNotNull;
import nl.topicus.jdbc.shaded.com.google.common.annotations.VisibleForTesting;
import nl.topicus.jdbc.shaded.com.google.common.base.Preconditions;
import nl.topicus.jdbc.shaded.com.google.common.base.Verify;
import nl.topicus.jdbc.shaded.io.grpc.Attributes;
import nl.topicus.jdbc.shaded.io.grpc.EquivalentAddressGroup;
import nl.topicus.jdbc.shaded.io.grpc.NameResolver;
import nl.topicus.jdbc.shaded.io.grpc.Status;
import nl.topicus.jdbc.shaded.io.grpc.internal.SharedResourceHolder.Resource;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import nl.topicus.jdbc.shaded.javax.annotation.Nullable;
import nl.topicus.jdbc.shaded.javax.annotation.concurrent.GuardedBy;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.InitialDirContext;
/**
* A DNS-based {@link NameResolver}.
*
* Each {@code A} or {@code AAAA} record emits an {@link EquivalentAddressGroup} in the list
* passed to {@link NameResolver.Listener#onAddresses(List, Attributes)}
*
* @see DnsNameResolverProvider
*/
final class DnsNameResolver extends NameResolver {
private static final Logger logger = Logger.getLogger(DnsNameResolver.class.getName());
private static final boolean JNDI_AVAILABLE = jndiAvailable();
private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage";
private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage";
private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname";
private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig";
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
static final String SERVICE_CONFIG_PREFIX = "_grpc_config=";
private static final Set SERVICE_CONFIG_CHOICE_KEYS =
Collections.unmodifiableSet(
new HashSet(
Arrays.asList(
SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY,
SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY,
SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY,
SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY)));
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
// From https://github.com/grpc/proposal/blob/master/A5-grpclb-in-dns.md
private static final String GRPCLB_NAME_PREFIX = "_grpclb._tcp.";
private static final String JNDI_PROPERTY =
System.getProperty("nl.topicus.jdbc.shaded.io.grpc.internal.DnsNameResolverProvider.enable_jndi", "false");
@VisibleForTesting
static boolean enableJndi = Boolean.parseBoolean(JNDI_PROPERTY);
@VisibleForTesting
final ProxyDetector proxyDetector;
/** Access through {@link #getLocalHostname}. */
private static String localHostname;
private final Random random = new Random();
private DelegateResolver delegateResolver = pickDelegateResolver();
private final String authority;
private final String host;
private final int port;
private final Resource executorResource;
@GuardedBy("this")
private boolean shutdown;
@GuardedBy("this")
private ExecutorService executor;
@GuardedBy("this")
private boolean resolving;
@GuardedBy("this")
private Listener listener;
DnsNameResolver(@Nullable String nsAuthority, String name, Attributes params,
Resource executorResource,
ProxyDetector proxyDetector) {
// TODO: if a DNS server is provided as nsAuthority, use it.
// https://www.captechconsulting.com/blogs/accessing-the-dusty-corners-of-dns-with-java
this.executorResource = executorResource;
// Must prepend a "//" to the name when constructing a URI, otherwise it will be treated as an
// opaque URI, thus the authority and host of the resulted URI would be null.
URI nameUri = URI.create("//" + name);
authority = Preconditions.checkNotNull(nameUri.getAuthority(),
"nameUri (%s) doesn't have an authority", nameUri);
host = Preconditions.checkNotNull(nameUri.getHost(), "host");
if (nameUri.getPort() == -1) {
Integer defaultPort = params.get(NameResolver.Factory.PARAMS_DEFAULT_PORT);
if (defaultPort != null) {
port = defaultPort;
} else {
throw new IllegalArgumentException(
"name '" + name + "' doesn't contain a port, and default port is not set in params");
}
} else {
port = nameUri.getPort();
}
this.proxyDetector = proxyDetector;
}
@Override
public final String getServiceAuthority() {
return authority;
}
@Override
public final synchronized void start(Listener listener) {
Preconditions.checkState(this.listener == null, "already started");
executor = SharedResourceHolder.get(executorResource);
this.listener = Preconditions.checkNotNull(listener, "listener");
resolve();
}
@Override
public final synchronized void refresh() {
Preconditions.checkState(listener != null, "not started");
resolve();
}
private final Runnable resolutionRunnable = new Runnable() {
@Override
public void run() {
Listener savedListener;
synchronized (DnsNameResolver.this) {
if (shutdown) {
return;
}
savedListener = listener;
resolving = true;
}
try {
InetSocketAddress destination = InetSocketAddress.createUnresolved(host, port);
ProxyParameters proxy;
try {
proxy = proxyDetector.proxyFor(destination);
} catch (IOException e) {
savedListener.onError(
Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));
return;
}
if (proxy != null) {
EquivalentAddressGroup server =
new EquivalentAddressGroup(
new PairSocketAddress(
destination,
Attributes
.newBuilder()
.set(ProxyDetector.PROXY_PARAMS_KEY, proxy)
.build()));
savedListener.onAddresses(Collections.singletonList(server), Attributes.EMPTY);
return;
}
ResolutionResults resolvedInetAddrs;
try {
resolvedInetAddrs = delegateResolver.resolve(host);
} catch (Exception e) {
savedListener.onError(
Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));
return;
}
// Each address forms an EAG
List servers = new ArrayList();
for (InetAddress inetAddr : resolvedInetAddrs.addresses) {
servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
}
servers.addAll(resolvedInetAddrs.balancerAddresses);
Attributes.Builder attrs = Attributes.newBuilder();
if (!resolvedInetAddrs.txtRecords.isEmpty()) {
Map serviceConfig = null;
try {
for (Map possibleConfig :
parseTxtResults(resolvedInetAddrs.txtRecords)) {
try {
serviceConfig =
maybeChooseServiceConfig(possibleConfig, random, getLocalHostname());
} catch (RuntimeException e) {
logger.log(Level.WARNING, "Bad service config choice " + possibleConfig, e);
}
if (serviceConfig != null) {
break;
}
}
} catch (RuntimeException e) {
logger.log(Level.WARNING, "Can't parse service Configs", e);
}
if (serviceConfig != null) {
attrs.set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig);
}
} else {
logger.log(Level.FINE, "No TXT records found for {0}", new Object[]{host});
}
savedListener.onAddresses(servers, attrs.build());
} finally {
synchronized (DnsNameResolver.this) {
resolving = false;
}
}
}
};
@GuardedBy("this")
private void resolve() {
if (resolving || shutdown) {
return;
}
executor.execute(resolutionRunnable);
}
@Override
public final synchronized void shutdown() {
if (shutdown) {
return;
}
shutdown = true;
if (executor != null) {
executor = SharedResourceHolder.release(executorResource, executor);
}
}
final int getPort() {
return port;
}
private DelegateResolver pickDelegateResolver() {
JdkResolver jdkResolver = new JdkResolver();
if (JNDI_AVAILABLE && enableJndi) {
return new CompositeResolver(jdkResolver, new JndiResolver());
}
return jdkResolver;
}
/**
* Forces the resolver. This should only be used by testing code.
*/
@VisibleForTesting
void setDelegateResolver(DelegateResolver delegateResolver) {
this.delegateResolver = delegateResolver;
}
@SuppressWarnings("unchecked")
@VisibleForTesting
static List