blob: 490bd8255aa3762916aeb124db2afe3015249e59 [file] [log] [blame]
/*
* 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.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.base.Verify;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
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.ExecutorService;
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}.
*
* <p>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<String> SERVICE_CONFIG_CHOICE_KEYS =
Collections.unmodifiableSet(
new HashSet<String>(
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_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.
*
* <p>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}.
*/
@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 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> resourceResolver =
new AtomicReference<ResourceResolver>();
private final String authority;
private final String host;
private final int port;
private final Resource<ExecutorService> executorResource;
private final long networkAddressCacheTtlNanos;
private final Stopwatch stopwatch;
@GuardedBy("this")
private boolean shutdown;
@GuardedBy("this")
private ExecutorService executor;
@GuardedBy("this")
private boolean resolving;
@GuardedBy("this")
private Listener listener;
private ResolutionResults cachedResolutionResults;
DnsNameResolver(@Nullable String nsAuthority, String name, Attributes params,
Resource<ExecutorService> executorResource, ProxyDetector proxyDetector,
Stopwatch stopwatch) {
// 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) {
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;
this.stopwatch = Preconditions.checkNotNull(stopwatch, "stopwatch");
this.networkAddressCacheTtlNanos = getNetworkAddressCacheTtlNanos();
}
@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;
}
boolean resourceRefreshRequired = cachedResolutionResults == null
|| networkAddressCacheTtlNanos == 0
|| (networkAddressCacheTtlNanos > 0
&& stopwatch.elapsed(TimeUnit.NANOSECONDS) > networkAddressCacheTtlNanos);
if (!resourceRefreshRequired) {
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 ProxySocketAddress(destination, proxy));
savedListener.onAddresses(Collections.singletonList(server), Attributes.EMPTY);
return;
}
ResolutionResults resolutionResults;
try {
ResourceResolver resourceResolver = null;
if (enableJndi) {
resourceResolver = getResourceResolver();
}
resolutionResults =
resolveAll(addressResolver, resourceResolver, enableSrv, enableTxt, host);
cachedResolutionResults = resolutionResults;
if (networkAddressCacheTtlNanos > 0) {
stopwatch.reset().start();
}
} catch (Exception e) {
savedListener.onError(
Status.UNAVAILABLE.withDescription("Unable to resolve host " + host).withCause(e));
return;
}
// Each address forms an EAG
List<EquivalentAddressGroup> servers = new ArrayList<>();
for (InetAddress inetAddr : resolutionResults.addresses) {
servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port)));
}
servers.addAll(resolutionResults.balancerAddresses);
Attributes.Builder attrs = Attributes.newBuilder();
if (!resolutionResults.txtRecords.isEmpty()) {
Map<String, Object> serviceConfig = null;
try {
for (Map<String, Object> possibleConfig :
parseTxtResults(resolutionResults.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;
}
}
}
};
/** Returns value of network address cache ttl property. */
private static long getNetworkAddressCacheTtlNanos() {
String cacheTtlPropertyValue = System.getProperty(NETWORKADDRESS_CACHE_TTL_PROPERTY);
long cacheTtl = DEFAULT_NETWORK_CACHE_TTL_SECONDS;
if (cacheTtlPropertyValue != null) {
try {
cacheTtl = Long.parseLong(cacheTtlPropertyValue);
} catch (NumberFormatException e) {
logger.log(
Level.WARNING,
"Property({0}) valid is not valid number format({1}), fall back to default({2})",
new Object[] {NETWORKADDRESS_CACHE_TTL_PROPERTY, cacheTtlPropertyValue, cacheTtl});
}
}
return cacheTtl > 0 ? TimeUnit.SECONDS.toNanos(cacheTtl) : cacheTtl;
}
@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;
}
@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<EquivalentAddressGroup> balancerAddresses = Collections.emptyList();
Exception balancerAddressesException = null;
List<String> 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) {
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<Map<String, Object>> parseTxtResults(List<String> txtRecords) {
List<Map<String, Object>> serviceConfigs = new ArrayList<Map<String, Object>>();
for (String txtRecord : txtRecords) {
if (txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
List<Map<String, Object>> choices;
try {
Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
if (!(rawChoices instanceof List)) {
throw new IOException("wrong type " + rawChoices);
}
List<Object> listChoices = (List<Object>) rawChoices;
for (Object obj : listChoices) {
if (!(obj instanceof Map)) {
throw new IOException("wrong element type " + rawChoices);
}
}
choices = (List<Map<String, Object>>) (List<?>) listChoices;
} catch (IOException e) {
logger.log(Level.WARNING, "Bad service config: " + txtRecord, e);
continue;
}
serviceConfigs.addAll(choices);
} else {
logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
}
}
return serviceConfigs;
}
@Nullable
private static final Double getPercentageFromChoice(
Map<String, Object> serviceConfigChoice) {
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY)) {
return null;
}
return ServiceConfigUtil.getDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
}
@Nullable
private static final List<String> getClientLanguagesFromChoice(
Map<String, Object> serviceConfigChoice) {
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY)) {
return null;
}
return ServiceConfigUtil.checkStringList(
ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY));
}
@Nullable
private static final List<String> getHostnamesFromChoice(
Map<String, Object> serviceConfigChoice) {
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY)) {
return null;
}
return ServiceConfigUtil.checkStringList(
ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY));
}
/**
* Determines if a given Service Config choice applies, and if so, returns it.
*
* @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
* Service Config in DNS</a>
* @param choice The service config choice.
* @return The service config object or {@code null} if this choice does not apply.
*/
@Nullable
@SuppressWarnings("BetaApi") // Verify isn't all that beta
@VisibleForTesting
static Map<String, Object> maybeChooseServiceConfig(
Map<String, Object> choice, Random random, String hostname) {
for (Entry<String, ?> entry : choice.entrySet()) {
Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
}
List<String> clientLanguages = getClientLanguagesFromChoice(choice);
if (clientLanguages != null && !clientLanguages.isEmpty()) {
boolean javaPresent = false;
for (String lang : clientLanguages) {
if ("java".equalsIgnoreCase(lang)) {
javaPresent = true;
break;
}
}
if (!javaPresent) {
return null;
}
}
Double percentage = getPercentageFromChoice(choice);
if (percentage != null) {
int pct = percentage.intValue();
Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage);
if (random.nextInt(100) >= pct) {
return null;
}
}
List<String> clientHostnames = getHostnamesFromChoice(choice);
if (clientHostnames != null && !clientHostnames.isEmpty()) {
boolean hostnamePresent = false;
for (String clientHostname : clientHostnames) {
if (clientHostname.equals(hostname)) {
hostnamePresent = true;
break;
}
}
if (!hostnamePresent) {
return null;
}
}
return ServiceConfigUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY);
}
/**
* Describes the results from a DNS query.
*/
@VisibleForTesting
static final class ResolutionResults {
final List<? extends InetAddress> addresses;
final List<String> txtRecords;
final List<EquivalentAddressGroup> balancerAddresses;
ResolutionResults(
List<? extends InetAddress> addresses,
List<String> txtRecords,
List<EquivalentAddressGroup> balancerAddresses) {
this.addresses = Collections.unmodifiableList(checkNotNull(addresses, "addresses"));
this.txtRecords = Collections.unmodifiableList(checkNotNull(txtRecords, "txtRecords"));
this.balancerAddresses =
Collections.unmodifiableList(checkNotNull(balancerAddresses, "balancerAddresses"));
}
}
@VisibleForTesting
void setAddressResolver(AddressResolver addressResolver) {
this.addressResolver = addressResolver;
}
/**
* {@link ResourceResolverFactory} is a factory for making resource resolvers. It supports
* optionally checking if the factory is available.
*/
interface ResourceResolverFactory {
/**
* Creates a new resource resolver. The return value is {@code null} iff
* {@link #unavailabilityCause()} is not null;
*/
@Nullable ResourceResolver newResourceResolver();
/**
* Returns the reason why the resource resolver cannot be created. The return value is
* {@code null} if {@link #newResourceResolver()} is suitable for use.
*/
@Nullable Throwable unavailabilityCause();
}
/**
* AddressResolver resolves a hostname into a list of addresses.
*/
interface AddressResolver {
List<InetAddress> resolveAddress(String host) throws Exception;
}
private enum JdkAddressResolver implements AddressResolver {
INSTANCE;
@Override
public List<InetAddress> resolveAddress(String host) throws UnknownHostException {
return Collections.unmodifiableList(Arrays.asList(InetAddress.getAllByName(host)));
}
}
/**
* {@link ResourceResolver} is a Dns ResourceRecord resolver.
*/
interface ResourceResolver {
List<String> resolveTxt(String host) throws Exception;
List<EquivalentAddressGroup> resolveSrv(
AddressResolver addressResolver, String host) throws Exception;
}
@Nullable
private ResourceResolver getResourceResolver() {
ResourceResolver rr;
if ((rr = resourceResolver.get()) == null) {
if (resourceResolverFactory != null) {
assert resourceResolverFactory.unavailabilityCause() == null;
rr = resourceResolverFactory.newResourceResolver();
}
}
return rr;
}
@Nullable
@VisibleForTesting
static ResourceResolverFactory getResourceResolverFactory(ClassLoader loader) {
Class<? extends ResourceResolverFactory> jndiClazz;
try {
jndiClazz =
Class.forName("io.grpc.internal.JndiResourceResolverFactory", true, loader)
.asSubclass(ResourceResolverFactory.class);
} catch (ClassNotFoundException e) {
logger.log(Level.FINE, "Unable to find JndiResourceResolverFactory, skipping.", e);
return null;
}
Constructor<? extends ResourceResolverFactory> jndiCtor;
try {
jndiCtor = jndiClazz.getConstructor();
} catch (Exception e) {
logger.log(Level.FINE, "Can't find JndiResourceResolverFactory ctor, skipping.", e);
return null;
}
ResourceResolverFactory rrf;
try {
rrf = jndiCtor.newInstance();
} catch (Exception e) {
logger.log(Level.FINE, "Can't construct JndiResourceResolverFactory, skipping.", e);
return null;
}
if (rrf.unavailabilityCause() != null) {
logger.log(
Level.FINE,
"JndiResourceResolverFactory not available, skipping.",
rrf.unavailabilityCause());
}
return rrf;
}
private static String getLocalHostname() {
if (localHostname == null) {
try {
localHostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
throw new RuntimeException(e);
}
}
return localHostname;
}
}