diff --git a/core/src/main/java/io/grpc/internal/DnsNameResolver.java b/core/src/main/java/io/grpc/internal/DnsNameResolver.java index a0faf90300..9ca30f2c2d 100644 --- a/core/src/main/java/io/grpc/internal/DnsNameResolver.java +++ b/core/src/main/java/io/grpc/internal/DnsNameResolver.java @@ -20,6 +20,7 @@ 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.Verify; import io.grpc.Attributes; import io.grpc.EquivalentAddressGroup; import io.grpc.NameResolver; @@ -27,7 +28,9 @@ import io.grpc.Status; import io.grpc.internal.SharedResourceHolder.Resource; 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; @@ -38,6 +41,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.naming.NamingEnumeration; @@ -49,7 +53,7 @@ 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#onUpdate} + * passed to {@link NameResolver.Listener#onAddresses(List, Attributes)} * * @see DnsNameResolverProvider */ @@ -59,8 +63,16 @@ final class DnsNameResolver extends NameResolver { private static final boolean JNDI_AVAILABLE = jndiAvailable(); + // 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 jndiProperty = + System.getProperty("io.grpc.internal.DnsNameResolverProvider.enable_jndi", "false"); + @VisibleForTesting - static boolean enableJndi = false; + static boolean enableJndi = Boolean.parseBoolean(jndiProperty); private DelegateResolver delegateResolver = pickDelegateResolver(); @@ -174,11 +186,19 @@ final class DnsNameResolver extends NameResolver { return; } // Each address forms an EAG - ArrayList servers = new ArrayList(); + List servers = new ArrayList(); for (InetAddress inetAddr : resolvedInetAddrs.addresses) { servers.add(new EquivalentAddressGroup(new InetSocketAddress(inetAddr, port))); } - savedListener.onAddresses(servers, Attributes.EMPTY); + servers.addAll(resolvedInetAddrs.balancerAddresses); + + Attributes.Builder attrs = Attributes.newBuilder(); + if (!resolvedInetAddrs.txtRecords.isEmpty()) { + attrs.set( + GrpcAttributes.NAME_RESOLVER_ATTR_DNS_TXT, + Collections.unmodifiableList(new ArrayList(resolvedInetAddrs.txtRecords))); + } + savedListener.onAddresses(servers, attrs.build()); } finally { synchronized (DnsNameResolver.this) { resolving = false; @@ -278,10 +298,16 @@ final class DnsNameResolver extends NameResolver { static final class ResolutionResults { final List addresses; final List txtRecords; + final List balancerAddresses; - ResolutionResults(List addresses, List txtRecords) { + ResolutionResults( + List addresses, + List txtRecords, + List balancerAddresses) { this.addresses = Collections.unmodifiableList(checkNotNull(addresses, "addresses")); this.txtRecords = Collections.unmodifiableList(checkNotNull(txtRecords, "txtRecords")); + this.balancerAddresses = + Collections.unmodifiableList(checkNotNull(balancerAddresses, "balancerAddresses")); } } @@ -305,14 +331,16 @@ final class DnsNameResolver extends NameResolver { ResolutionResults jdkResults = jdkResovler.resolve(host); List addresses = jdkResults.addresses; List txtRecords = Collections.emptyList(); + List balancerAddresses = Collections.emptyList(); try { ResolutionResults jdniResults = jndiResovler.resolve(host); txtRecords = jdniResults.txtRecords; + balancerAddresses = jdniResults.balancerAddresses; } catch (Exception e) { logger.log(Level.SEVERE, "Failed to resolve TXT results", e); } - return new ResolutionResults(addresses, txtRecords); + return new ResolutionResults(addresses, txtRecords, balancerAddresses); } } @@ -328,7 +356,8 @@ final class DnsNameResolver extends NameResolver { ResolutionResults resolve(String host) throws Exception { return new ResolutionResults( Arrays.asList(InetAddress.getAllByName(host)), - Collections.emptyList()); + Collections.emptyList(), + Collections.emptyList()); } } @@ -340,26 +369,84 @@ final class DnsNameResolver extends NameResolver { @VisibleForTesting static final class JndiResolver extends DelegateResolver { - private static final String[] rrTypes = new String[]{"TXT"}; + private static final Pattern whitespace = Pattern.compile("\\s+"); @Override ResolutionResults resolve(String host) throws NamingException { + List serviceConfigTxtRecords = Collections.emptyList(); + String serviceConfigHostname = SERVICE_CONFIG_NAME_PREFIX + host; + if (logger.isLoggable(Level.FINER)) { + logger.log( + Level.FINER, "About to query TXT records for {0}", new Object[]{serviceConfigHostname}); + } + try { + serviceConfigTxtRecords = getAllRecords("TXT", "dns:///" + serviceConfigHostname); + } catch (NamingException e) { + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Unable to look up " + serviceConfigHostname, e); + } + } + + String grpclbHostname = GRPCLB_NAME_PREFIX + host; + if (logger.isLoggable(Level.FINER)) { + logger.log( + Level.FINER, "About to query SRV records for {0}", new Object[]{grpclbHostname}); + } + List balancerAddresses = Collections.emptyList(); + try { + List grpclbSrvRecords = getAllRecords("SRV", "dns:///" + grpclbHostname); + balancerAddresses = new ArrayList(grpclbSrvRecords.size()); + for (String srvRecord : grpclbSrvRecords) { + try { + String[] parts = whitespace.split(srvRecord); + Verify.verify(parts.length == 4, "Bad SRV Record: %s, ", srvRecord); + String srvHostname = parts[3]; + int port = Integer.parseInt(parts[2]); + + InetAddress[] addrs = InetAddress.getAllByName(srvHostname); + List sockaddrs = new ArrayList(addrs.length); + for (InetAddress addr : addrs) { + sockaddrs.add(new InetSocketAddress(addr, port)); + } + Attributes attrs = Attributes.newBuilder() + .set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, srvHostname) + .build(); + balancerAddresses.add( + new EquivalentAddressGroup(Collections.unmodifiableList(sockaddrs), attrs)); + } catch (UnknownHostException e) { + logger.log(Level.WARNING, "Can't find address for SRV record" + srvRecord, e); + } catch (RuntimeException e) { + logger.log(Level.WARNING, "Failed to construct SRV record" + srvRecord, e); + } + } + } catch (NamingException e) { + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Unable to look up " + serviceConfigHostname, e); + } + } + + return new ResolutionResults( + /*addresses=*/ Collections.emptyList(), + serviceConfigTxtRecords, + Collections.unmodifiableList(balancerAddresses)); + } + + private List getAllRecords(String recordType, String name) throws NamingException { InitialDirContext dirContext = new InitialDirContext(); - javax.naming.directory.Attributes attrs = dirContext.getAttributes("dns:///" + host, rrTypes); - List addresses = new ArrayList(); - List txtRecords = new ArrayList(); + String[] rrType = new String[]{recordType}; + javax.naming.directory.Attributes attrs = dirContext.getAttributes(name, rrType); + List records = new ArrayList(); NamingEnumeration rrGroups = attrs.getAll(); try { while (rrGroups.hasMore()) { Attribute rrEntry = rrGroups.next(); - assert Arrays.asList(rrTypes).contains(rrEntry.getID()); + assert Arrays.asList(rrType).contains(rrEntry.getID()); NamingEnumeration rrValues = rrEntry.getAll(); try { while (rrValues.hasMore()) { - String rrValue = (String) rrValues.next(); - txtRecords.add(rrValue); + records.add(String.valueOf(rrValues.next())); } } finally { rrValues.close(); @@ -368,8 +455,7 @@ final class DnsNameResolver extends NameResolver { } finally { rrGroups.close(); } - - return new ResolutionResults(addresses, txtRecords); + return records; } } } diff --git a/core/src/main/java/io/grpc/internal/GrpcAttributes.java b/core/src/main/java/io/grpc/internal/GrpcAttributes.java new file mode 100644 index 0000000000..b6d5e8a86a --- /dev/null +++ b/core/src/main/java/io/grpc/internal/GrpcAttributes.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017, gRPC Authors All rights reserved. + * + * 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 io.grpc.Attributes; +import java.util.List; + +/** + * Special attributes that are only useful to gRPC. + */ +public final class GrpcAttributes { + /** + * Attribute key TXT DNS records. + */ + public static final Attributes.Key> NAME_RESOLVER_ATTR_DNS_TXT = + Attributes.Key.of("dns-txt"); + + /** + * The naming authority of a gRPC LB server address. It is an address-group-level attribute, + * present when the address group is a LoadBalancer. + */ + public static final Attributes.Key ATTR_LB_ADDR_AUTHORITY = + Attributes.Key.of("io.grpc.grpclb.lbAddrAuthority"); + + + private GrpcAttributes() {} +} diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 5c39dc759f..4458dd6c2b 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -906,27 +906,32 @@ public final class ManagedChannelImpl extends ManagedChannel { onError(Status.UNAVAILABLE.withDescription("NameResolver returned an empty list")); return; } - logger.log(Level.FINE, "[{0}] resolved address: {1}, config={2}", - new Object[] {getLogId(), servers, config}); - helper.runSerialized(new Runnable() { - @Override - public void run() { - // Call LB only if it's not shutdown. If LB is shutdown, lbHelper won't match. - if (NameResolverListenerImpl.this.helper != ManagedChannelImpl.this.lbHelper) { - return; - } - try { - balancer.handleResolvedAddressGroups(servers, config); - } catch (Throwable e) { - logger.log( - Level.WARNING, "[" + getLogId() + "] Unexpected exception from LoadBalancer", e); - // It must be a bug! Push the exception back to LoadBalancer in the hope that it may - // be propagated to the application. - balancer.handleNameResolutionError(Status.INTERNAL.withCause(e) - .withDescription("Thrown from handleResolvedAddresses(): " + e)); - } + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "[{0}] resolved address: {1}, config={2}", + new Object[]{getLogId(), servers, config}); + } + + final class NamesResolved implements Runnable { + @Override + public void run() { + // Call LB only if it's not shutdown. If LB is shutdown, lbHelper won't match. + if (NameResolverListenerImpl.this.helper != ManagedChannelImpl.this.lbHelper) { + return; } - }); + try { + balancer.handleResolvedAddressGroups(servers, config); + } catch (Throwable e) { + logger.log( + Level.WARNING, "[" + getLogId() + "] Unexpected exception from LoadBalancer", e); + // It must be a bug! Push the exception back to LoadBalancer in the hope that it may + // be propagated to the application. + balancer.handleNameResolutionError(Status.INTERNAL.withCause(e) + .withDescription("Thrown from handleResolvedAddresses(): " + e)); + } + } + } + + helper.runSerialized(new NamesResolved()); } @Override diff --git a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java index ee0324f1de..7183217e3b 100644 --- a/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java +++ b/core/src/test/java/io/grpc/internal/DnsNameResolverTest.java @@ -323,15 +323,25 @@ public class DnsNameResolverTest { DelegateResolver resolver = new DnsNameResolver.CompositeResolver(jdkDelegate, jndiDelegate); List jdkAnswer = createAddressList(2); - jdkDelegate.addAnswer(jdkAnswer, Arrays.asList("jdktxt")); + jdkDelegate.addAnswer( + jdkAnswer, + Arrays.asList("jdktxt"), + Collections.emptyList()); List jdniAnswer = createAddressList(2); - jndiDelegate.addAnswer(jdniAnswer, Arrays.asList("jnditxt")); + jndiDelegate.addAnswer( + jdniAnswer, + Arrays.asList("jnditxt"), + Collections.singletonList( + new EquivalentAddressGroup( + Collections.singletonList(new SocketAddress() {}), + Attributes.EMPTY))); ResolutionResults results = resolver.resolve("abc"); assertThat(results.addresses).containsExactlyElementsIn(jdkAnswer).inOrder(); assertThat(results.txtRecords).containsExactly("jnditxt"); + assertThat(results.balancerAddresses).hasSize(1); } @Test @@ -418,14 +428,19 @@ public class DnsNameResolverTest { private final Queue invocations = new LinkedList(); MockResolver addAnswer(List addresses) { - return addAnswer(addresses, null); + return addAnswer(addresses, null, null); } - MockResolver addAnswer(List addresses, List txtRecords) { + MockResolver addAnswer( + List addresses, + List txtRecords, + List balancerAddresses) { answers.add( new ResolutionResults( addresses, - MoreObjects.firstNonNull(txtRecords, Collections.emptyList()))); + MoreObjects.firstNonNull(txtRecords, Collections.emptyList()), + MoreObjects.firstNonNull( + balancerAddresses, Collections.emptyList()))); return this; } diff --git a/grpclb/src/main/java/io/grpc/grpclb/GrpclbConstants.java b/grpclb/src/main/java/io/grpc/grpclb/GrpclbConstants.java index 99745aac7a..4e72074660 100644 --- a/grpclb/src/main/java/io/grpc/grpclb/GrpclbConstants.java +++ b/grpclb/src/main/java/io/grpc/grpclb/GrpclbConstants.java @@ -40,13 +40,6 @@ public final class GrpclbConstants { public static final Attributes.Key ATTR_LB_POLICY = Attributes.Key.of("io.grpc.grpclb.lbPolicy"); - /** - * The naming authority of an LB server address. It is an address-group-level attribute, present - * when the address group is a LoadBalancer. - */ - public static final Attributes.Key ATTR_LB_ADDR_AUTHORITY = - Attributes.Key.of("io.grpc.grpclb.lbAddrAuthority"); - /** * The opaque token given by the remote balancer for each returned server address. The client * will send this token with any requests sent to the associated server. diff --git a/grpclb/src/main/java/io/grpc/grpclb/GrpclbLoadBalancer.java b/grpclb/src/main/java/io/grpc/grpclb/GrpclbLoadBalancer.java index ec7326eecb..adc280bb5d 100644 --- a/grpclb/src/main/java/io/grpc/grpclb/GrpclbLoadBalancer.java +++ b/grpclb/src/main/java/io/grpc/grpclb/GrpclbLoadBalancer.java @@ -27,6 +27,7 @@ import io.grpc.InternalWithLogId; import io.grpc.LoadBalancer; import io.grpc.Status; import io.grpc.grpclb.GrpclbConstants.LbPolicy; +import io.grpc.internal.GrpcAttributes; import io.grpc.internal.ObjectPool; import java.util.ArrayList; import java.util.Collections; @@ -104,7 +105,7 @@ class GrpclbLoadBalancer extends LoadBalancer implements InternalWithLogId { List newLbAddressGroups = new ArrayList(); List newBackendServers = new ArrayList(); for (EquivalentAddressGroup server : updatedServers) { - String lbAddrAuthority = server.getAttributes().get(GrpclbConstants.ATTR_LB_ADDR_AUTHORITY); + String lbAddrAuthority = server.getAttributes().get(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY); if (lbAddrAuthority != null) { newLbAddressGroups.add(new LbAddressGroup(server, lbAddrAuthority)); } else { diff --git a/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java b/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java index 3feb985f93..41c081a1ce 100644 --- a/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java +++ b/grpclb/src/test/java/io/grpc/grpclb/GrpclbLoadBalancerTest.java @@ -71,6 +71,7 @@ import io.grpc.grpclb.GrpclbState.RoundRobinPicker; import io.grpc.inprocess.InProcessChannelBuilder; import io.grpc.inprocess.InProcessServerBuilder; import io.grpc.internal.FakeClock; +import io.grpc.internal.GrpcAttributes; import io.grpc.internal.ObjectPool; import io.grpc.internal.SerializingExecutor; import io.grpc.stub.StreamObserver; @@ -1623,7 +1624,7 @@ public class GrpclbLoadBalancerTest { private static Attributes lbAttributes(String authority) { return Attributes.newBuilder() - .set(GrpclbConstants.ATTR_LB_ADDR_AUTHORITY, authority) + .set(GrpcAttributes.ATTR_LB_ADDR_AUTHORITY, authority) .build(); }