io.grpc.internal.DnsNameResolver Maven / Gradle / Ivy
/*
* 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 io.grpc.internal;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
import com.google.common.base.Verify;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.ProxiedSocketAddress;
import io.grpc.ProxyDetector;
import io.grpc.Status;
import io.grpc.internal.SharedResourceHolder.Resource;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
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.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
/**
* 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 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("io.grpc.internal.DnsNameResolverProvider.enable_jndi", "true");
private static final String JNDI_LOCALHOST_PROPERTY =
System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi_localhost", "false");
private static final String JNDI_SRV_PROPERTY =
System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_grpclb", "false");
private static final String JNDI_TXT_PROPERTY =
System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_service_config", "false");
/**
* Java networking system properties name for caching DNS result.
*
* Default value is -1 (cache forever) if security manager is installed. If security manager is
* not installed, the ttl value is {@code null} which falls back to {@link
* #DEFAULT_NETWORK_CACHE_TTL_SECONDS gRPC default value}.
*
*
For android, gRPC doesn't attempt to cache; this property value will be ignored.
*/
@VisibleForTesting
static final String NETWORKADDRESS_CACHE_TTL_PROPERTY = "networkaddress.cache.ttl";
/** Default DNS cache duration if network cache ttl value is not specified ({@code null}). */
@VisibleForTesting
static final long DEFAULT_NETWORK_CACHE_TTL_SECONDS = 30;
@VisibleForTesting
static boolean enableJndi = Boolean.parseBoolean(JNDI_PROPERTY);
@VisibleForTesting
static boolean enableJndiLocalhost = Boolean.parseBoolean(JNDI_LOCALHOST_PROPERTY);
@VisibleForTesting
static boolean enableSrv = Boolean.parseBoolean(JNDI_SRV_PROPERTY);
@VisibleForTesting
static boolean enableTxt = Boolean.parseBoolean(JNDI_TXT_PROPERTY);
private static final ResourceResolverFactory resourceResolverFactory =
getResourceResolverFactory(DnsNameResolver.class.getClassLoader());
@VisibleForTesting
final ProxyDetector proxyDetector;
/** Access through {@link #getLocalHostname}. */
private static String localHostname;
private final Random random = new Random();
private volatile AddressResolver addressResolver = JdkAddressResolver.INSTANCE;
private final AtomicReference resourceResolver = new AtomicReference<>();
private final String authority;
private final String host;
private final int port;
private final Resource executorResource;
@GuardedBy("this")
private boolean shutdown;
@GuardedBy("this")
private Executor executor;
@GuardedBy("this")
private boolean resolving;
@GuardedBy("this")
private Listener listener;
private final Runnable resolveRunnable;
DnsNameResolver(@Nullable String nsAuthority, String name, Helper helper,
Resource executorResource, Stopwatch stopwatch, boolean isAndroid) {
// 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("//" + checkNotNull(name, "name"));
Preconditions.checkArgument(nameUri.getHost() != null, "Invalid DNS name: %s", name);
authority = Preconditions.checkNotNull(nameUri.getAuthority(),
"nameUri (%s) doesn't have an authority", nameUri);
host = nameUri.getHost();
if (nameUri.getPort() == -1) {
port = helper.getDefaultPort();
} else {
port = nameUri.getPort();
}
this.proxyDetector = Preconditions.checkNotNull(helper.getProxyDetector(), "proxyDetector");
this.resolveRunnable = new Resolve(this, stopwatch, getNetworkAddressCacheTtlNanos(isAndroid));
}
@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();
}
@VisibleForTesting
static final class Resolve implements Runnable {
private final DnsNameResolver resolver;
private final Stopwatch stopwatch;
private final long cacheTtlNanos;
private ResolutionResults cachedResolutionResults = null;
Resolve(DnsNameResolver resolver, Stopwatch stopwatch, long cacheTtlNanos) {
this.resolver = resolver;
this.stopwatch = Preconditions.checkNotNull(stopwatch, "stopwatch");
this.cacheTtlNanos = cacheTtlNanos;
}
@Override
public void run() {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Attempting DNS resolution of " + resolver.host);
}
Listener savedListener;
synchronized (resolver) {
if (resolver.shutdown || !cacheRefreshRequired()) {
return;
}
savedListener = resolver.listener;
resolver.resolving = true;
}
try {
resolveInternal(savedListener);
} finally {
synchronized (resolver) {
resolver.resolving = false;
}
}
}
private boolean cacheRefreshRequired() {
return cachedResolutionResults == null
|| cacheTtlNanos == 0
|| (cacheTtlNanos > 0 && stopwatch.elapsed(TimeUnit.NANOSECONDS) > cacheTtlNanos);
}
@VisibleForTesting
void resolveInternal(Listener savedListener) {
InetSocketAddress destination =
InetSocketAddress.createUnresolved(resolver.host, resolver.port);
ProxiedSocketAddress proxiedAddr;
try {
proxiedAddr = resolver.proxyDetector.proxyFor(destination);
} catch (IOException e) {
savedListener.onError(
Status.UNAVAILABLE.withDescription("Unable to resolve host " + resolver.host)
.withCause(e));
return;
}
if (proxiedAddr != null) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Using proxy address " + proxiedAddr);
}
EquivalentAddressGroup server = new EquivalentAddressGroup(proxiedAddr);
savedListener.onAddresses(Collections.singletonList(server), Attributes.EMPTY);
return;
}
ResolutionResults resolutionResults;
try {
ResourceResolver resourceResolver = null;
if (shouldUseJndi(enableJndi, enableJndiLocalhost, resolver.host)) {
resourceResolver = resolver.getResourceResolver();
}
resolutionResults = resolveAll(
resolver.addressResolver,
resourceResolver,
enableSrv,
enableTxt,
resolver.host);
cachedResolutionResults = resolutionResults;
if (cacheTtlNanos > 0) {
stopwatch.reset().start();
}
if (logger.isLoggable(Level.FINER)) {
logger.finer("Found DNS results " + resolutionResults + " for " + resolver.host);
}
} catch (Exception e) {
savedListener.onError(
Status.UNAVAILABLE.withDescription("Unable to resolve host " + resolver.host)
.withCause(e));
return;
}
// Each address forms an EAG
List servers = new ArrayList<>();
for (InetAddress inetAddr : resolutionResults.addresses) {
servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, resolver.port)));
}
servers.addAll(resolutionResults.balancerAddresses);
if (servers.isEmpty()) {
savedListener.onError(Status.UNAVAILABLE.withDescription(
"No DNS backend or balancer addresses found for " + resolver.host));
return;
}
Attributes.Builder attrs = Attributes.newBuilder();
if (!resolutionResults.txtRecords.isEmpty()) {
Map serviceConfig = null;
try {
for (Map possibleConfig :
parseTxtResults(resolutionResults.txtRecords)) {
try {
serviceConfig =
maybeChooseServiceConfig(possibleConfig, resolver.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[]{resolver.host});
}
savedListener.onAddresses(servers, attrs.build());
}
}
@GuardedBy("this")
private void resolve() {
if (resolving || shutdown) {
return;
}
executor.execute(resolveRunnable);
}
@Override
public final synchronized void shutdown() {
if (shutdown) {
return;
}
shutdown = true;
if (executor != null) {
executor = SharedResourceHolder.release(executorResource, executor);
}
}
final int getPort() {
return port;
}
@VisibleForTesting
static ResolutionResults resolveAll(
AddressResolver addressResolver,
@Nullable ResourceResolver resourceResolver,
boolean requestSrvRecords,
boolean requestTxtRecords,
String name) {
List extends InetAddress> addresses = Collections.emptyList();
Exception addressesException = null;
List balancerAddresses = Collections.emptyList();
Exception balancerAddressesException = null;
List txtRecords = Collections.emptyList();
Exception txtRecordsException = null;
try {
addresses = addressResolver.resolveAddress(name);
} catch (Exception e) {
addressesException = e;
}
if (resourceResolver != null) {
if (requestSrvRecords) {
try {
balancerAddresses =
resourceResolver.resolveSrv(addressResolver, GRPCLB_NAME_PREFIX + name);
} catch (Exception e) {
balancerAddressesException = e;
}
}
if (requestTxtRecords) {
boolean balancerLookupFailedOrNotAttempted =
!requestSrvRecords || balancerAddressesException != null;
boolean dontResolveTxt =
(addressesException != null) && balancerLookupFailedOrNotAttempted;
// Only do the TXT record lookup if one of the above address resolutions succeeded.
if (!dontResolveTxt) {
try {
txtRecords = resourceResolver.resolveTxt(SERVICE_CONFIG_NAME_PREFIX + name);
} catch (Exception e) {
txtRecordsException = e;
}
}
}
}
try {
if (addressesException != null
&& (balancerAddressesException != null || balancerAddresses.isEmpty())) {
Throwables.throwIfUnchecked(addressesException);
throw new RuntimeException(addressesException);
}
} finally {
if (addressesException != null) {
logger.log(Level.FINE, "Address resolution failure", addressesException);
}
if (balancerAddressesException != null) {
logger.log(Level.FINE, "Balancer resolution failure", balancerAddressesException);
}
if (txtRecordsException != null) {
logger.log(Level.FINE, "ServiceConfig resolution failure", txtRecordsException);
}
}
return new ResolutionResults(addresses, txtRecords, balancerAddresses);
}
@SuppressWarnings("unchecked")
@VisibleForTesting
static List