diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java index 131a222b0e..2efdc6e205 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java @@ -80,13 +80,17 @@ abstract class DnsNameResolverContext { DnsNameResolverContext.class, "onResponseCNAME(..)"); private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( - new RuntimeException("No matching record type record found"), + new RuntimeException("No matching record type found"), DnsNameResolverContext.class, "onResponseAorAAAA(..)"); private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( new RuntimeException("Response type was unrecognized"), DnsNameResolverContext.class, "onResponse(..)"); + private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION = ThrowableUtil.unknownStackTrace( + new RuntimeException("No name servers returned an answer"), + DnsNameResolverContext.class, + "tryToFinishResolve(..)"); private final DnsNameResolver parent; private final DnsServerAddressStream nameServerAddrs; @@ -174,11 +178,15 @@ abstract class DnsNameResolverContext { private void internalResolve(Promise promise) { DnsServerAddressStream nameServerAddressStream = getNameServers(hostname); - for (DnsRecordType type: parent.resolveRecordTypes()) { - if (!query(hostname, type, nameServerAddressStream, promise)) { + DnsRecordType[] recordTypes = parent.resolveRecordTypes(); + assert recordTypes.length > 0; + final int end = recordTypes.length - 1; + for (int i = 0; i < end; ++i) { + if (!query(hostname, recordTypes[i], nameServerAddressStream.duplicate(), promise)) { return; } } + query(hostname, recordTypes[end], nameServerAddressStream, promise); } /** @@ -266,23 +274,25 @@ abstract class DnsNameResolverContext { } } - private void query(final DnsServerAddressStream nameServerAddrStream, final DnsQuestion question, + private void query(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex, + final DnsQuestion question, final Promise promise) { - query(nameServerAddrStream, question, parent.dnsQueryLifecycleObserverFactory() - .newDnsQueryLifecycleObserver(question), promise); + query(nameServerAddrStream, nameServerAddrStreamIndex, question, + parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question), promise); } - private void query(final DnsServerAddressStream nameServerAddrStream, final DnsQuestion question, + private void query(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, final DnsQueryLifecycleObserver queryLifecycleObserver, final Promise promise) { - if (allowedQueries == 0 || promise.isCancelled()) { - queryLifecycleObserver.queryCancelled(allowedQueries); - tryToFinishResolve(promise); + if (nameServerAddrStreamIndex >= nameServerAddrStream.size() || allowedQueries == 0 || promise.isCancelled()) { + tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver, + promise); return; } - allowedQueries --; - + --allowedQueries; final InetSocketAddress nameServerAddr = nameServerAddrStream.next(); final ChannelPromise writePromise = parent.ch.newPromise(); final Future> f = parent.query0( @@ -304,21 +314,26 @@ abstract class DnsNameResolverContext { try { if (future.isSuccess()) { - onResponse(nameServerAddrStream, question, future.getNow(), queryLifecycleObserver, promise); + onResponse(nameServerAddrStream, nameServerAddrStreamIndex, question, future.getNow(), + queryLifecycleObserver, promise); } else { // Server did not respond or I/O error occurred; try again. queryLifecycleObserver.queryFailed(future.cause()); - query(nameServerAddrStream, question, promise); + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, promise); } } finally { - tryToFinishResolve(promise); + tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, + // queryLifecycleObserver has already been terminated at this point so we must + // not allow it to be terminated again by tryToFinishResolve. + NoopDnsQueryLifecycleObserver.INSTANCE, + promise); } } }); } - void onResponse(final DnsServerAddressStream nameServerAddrStream, final DnsQuestion question, - AddressedEnvelope envelope, + void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex, + final DnsQuestion question, AddressedEnvelope envelope, final DnsQueryLifecycleObserver queryLifecycleObserver, Promise promise) { try { @@ -343,7 +358,8 @@ abstract class DnsNameResolverContext { // Retry with the next server if the server did not tell us that the domain does not exist. if (code != DnsResponseCode.NXDOMAIN) { - query(nameServerAddrStream, question, queryLifecycleObserver.queryNoAnswer(code), promise); + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, + queryLifecycleObserver.queryNoAnswer(code), promise); } else { queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION); } @@ -396,7 +412,7 @@ abstract class DnsNameResolverContext { } if (!nameServers.isEmpty()) { - query(parent.uncachedRedirectDnsServerStream(nameServers), question, + query(parent.uncachedRedirectDnsServerStream(nameServers), 0, question, queryLifecycleObserver.queryRedirected(unmodifiableList(nameServers)), promise); return true; } @@ -539,7 +555,7 @@ abstract class DnsNameResolverContext { } if (found) { - followCname(response.sender(), name, resolved, queryLifecycleObserver, promise); + followCname(resolved, queryLifecycleObserver, promise); } else { queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION); } @@ -575,8 +591,15 @@ abstract class DnsNameResolverContext { return cnames != null? cnames : Collections.emptyMap(); } - void tryToFinishResolve(Promise promise) { + void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + final DnsQueryLifecycleObserver queryLifecycleObserver, + final Promise promise) { + // There are no queries left to try. if (!queriesInProgress.isEmpty()) { + queryLifecycleObserver.queryCancelled(allowedQueries); + // There are still some queries we did not receive responses for. if (gotPreferredAddress()) { // But it's OK to finish the resolution process if we got a resolved address of the preferred type. @@ -589,6 +612,20 @@ abstract class DnsNameResolverContext { // There are no queries left to try. if (resolvedEntries == null) { + if (nameServerAddrStreamIndex < nameServerAddrStream.size()) { + if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) { + // If the queryLifecycleObserver has already been terminated we should create a new one for this + // fresh query. + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, promise); + } else { + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver, + promise); + } + return; + } + + queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION); + // .. and we could not find any A/AAAA records. if (!triedCNAME) { // As the last resort, try to query CNAME, just in case the name server has it. @@ -597,6 +634,8 @@ abstract class DnsNameResolverContext { query(hostname, DnsRecordType.CNAME, getNameServers(hostname), promise); return; } + } else { + queryLifecycleObserver.queryCancelled(allowedQueries); } // We have at least one resolved address or tried CNAME as the last resort.. @@ -688,9 +727,7 @@ abstract class DnsNameResolverContext { return stream == null ? nameServerAddrs : stream; } - private void followCname(InetSocketAddress nameServerAddr, String name, String cname, - final DnsQueryLifecycleObserver queryLifecycleObserver, - Promise promise) { + private void followCname(String cname, final DnsQueryLifecycleObserver queryLifecycleObserver, Promise promise) { // Use the same server for both CNAME queries DnsServerAddressStream stream = DnsServerAddresses.singleton(getNameServers(cname).next()).stream(); @@ -704,7 +741,7 @@ abstract class DnsNameResolverContext { queryLifecycleObserver.queryFailed(cause); PlatformDependent.throwException(cause); } - query(stream, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise); + query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise); } if (parent.supportsAAAARecords()) { try { @@ -715,7 +752,7 @@ abstract class DnsNameResolverContext { queryLifecycleObserver.queryFailed(cause); PlatformDependent.throwException(cause); } - query(stream, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise); + query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise); } } @@ -725,7 +762,7 @@ abstract class DnsNameResolverContext { if (question == null) { return false; } - query(dnsServerAddressStream, question, promise); + query(dnsServerAddressStream, 0, question, promise); return true; } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java index 3f04d69530..af014f9514 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java @@ -29,4 +29,19 @@ public interface DnsServerAddressStream { * Retrieves the next DNS server address from the stream. */ InetSocketAddress next(); + + /** + * Get the number of times {@link #next()} will return a distinct element before repeating or terminating. + * @return the number of times {@link #next()} will return a distinct element before repeating or terminating. + */ + int size(); + + /** + * Duplicate this object. The result of this should be able to be independently iterated over via {@link #next()}. + *

+ * Note that {@link #clone()} isn't used because it may make sense for some implementations to have the following + * relationship {@code x.duplicate() == x}. + * @return A duplicate of this object. + */ + DnsServerAddressStream duplicate(); } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStream.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStream.java index 6c1d84a016..b2288e3358 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStream.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStream.java @@ -40,6 +40,16 @@ final class SequentialDnsServerAddressStream implements DnsServerAddressStream { return next; } + @Override + public int size() { + return addresses.length; + } + + @Override + public SequentialDnsServerAddressStream duplicate() { + return new SequentialDnsServerAddressStream(addresses, i); + } + @Override public String toString() { return toString("sequential", i, addresses); diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java new file mode 100644 index 0000000000..a23aeba9fe --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.resolver.dns; + +import io.netty.util.internal.UnstableApi; + +import java.net.InetSocketAddress; + +import static io.netty.resolver.dns.DnsServerAddresses.sequential; + +/** + * A {@link DnsServerAddressStreamProvider} which is backed by a sequential list of DNS servers. + */ +@UnstableApi +public final class SequentialDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider { + /** + * Create a new instance. + * @param addresses The addresses which will be be returned in sequential order via + * {@link #nameServerAddressStream(String)} + */ + public SequentialDnsServerAddressStreamProvider(InetSocketAddress... addresses) { + super(sequential(addresses)); + } + + /** + * Create a new instance. + * @param addresses The addresses which will be be returned in sequential order via + * {@link #nameServerAddressStream(String)} + */ + public SequentialDnsServerAddressStreamProvider(Iterable addresses) { + super(sequential(addresses)); + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java b/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java index b0e2f0e85c..a30302e3ae 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java @@ -26,12 +26,22 @@ final class ShuffledDnsServerAddressStream implements DnsServerAddressStream { private final InetSocketAddress[] addresses; private int i; + /** + * Create a new instance. + * @param addresses The addresses are not cloned. It is assumed the caller has cloned this array or otherwise will + * not modify the contents. + */ ShuffledDnsServerAddressStream(InetSocketAddress[] addresses) { - this.addresses = addresses.clone(); + this.addresses = addresses; shuffle(); } + private ShuffledDnsServerAddressStream(InetSocketAddress[] addresses, int startIdx) { + this.addresses = addresses; + i = startIdx; + } + private void shuffle() { final InetSocketAddress[] addresses = this.addresses; final Random r = PlatformDependent.threadLocalRandom(); @@ -57,6 +67,16 @@ final class ShuffledDnsServerAddressStream implements DnsServerAddressStream { return next; } + @Override + public int size() { + return addresses.length; + } + + @Override + public ShuffledDnsServerAddressStream duplicate() { + return new ShuffledDnsServerAddressStream(addresses, i); + } + @Override public String toString() { return SequentialDnsServerAddressStream.toString("shuffled", i, addresses); diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java index c753fb266d..cfd5f8b3aa 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java @@ -23,19 +23,12 @@ import java.net.InetSocketAddress; * A {@link DnsServerAddressStreamProvider} which always uses a single DNS server for resolution. */ @UnstableApi -public final class SingletonDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { - private final DnsServerAddresses addresses; - +public final class SingletonDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider { /** * Create a new instance. * @param address The singleton address to use for every DNS resolution. */ public SingletonDnsServerAddressStreamProvider(final InetSocketAddress address) { - addresses = DnsServerAddresses.singleton(address); - } - - @Override - public DnsServerAddressStream nameServerAddressStream(String hostname) { - return addresses.stream(); + super(DnsServerAddresses.singleton(address)); } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddresses.java b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddresses.java index 4936d38890..0ef465c7fd 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddresses.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddresses.java @@ -21,7 +21,6 @@ import java.net.InetSocketAddress; final class SingletonDnsServerAddresses extends DnsServerAddresses { private final InetSocketAddress address; - private final String strVal; private final DnsServerAddressStream stream = new DnsServerAddressStream() { @Override @@ -29,6 +28,16 @@ final class SingletonDnsServerAddresses extends DnsServerAddresses { return address; } + @Override + public int size() { + return 1; + } + + @Override + public DnsServerAddressStream duplicate() { + return this; + } + @Override public String toString() { return SingletonDnsServerAddresses.this.toString(); @@ -37,7 +46,6 @@ final class SingletonDnsServerAddresses extends DnsServerAddresses { SingletonDnsServerAddresses(InetSocketAddress address) { this.address = address; - strVal = new StringBuilder(32).append("singleton(").append(address).append(')').toString(); } @Override @@ -47,6 +55,6 @@ final class SingletonDnsServerAddresses extends DnsServerAddresses { @Override public String toString() { - return strVal; + return "singleton(" + address + ")"; } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/UniSequentialDnsServerAddressStreamProvider.java b/resolver-dns/src/main/java/io/netty/resolver/dns/UniSequentialDnsServerAddressStreamProvider.java new file mode 100644 index 0000000000..5914f6363e --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/UniSequentialDnsServerAddressStreamProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you 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.netty.resolver.dns; + +import io.netty.util.internal.ObjectUtil; + +/** + * A {@link DnsServerAddressStreamProvider} which is backed by a single {@link DnsServerAddresses}. + */ +abstract class UniSequentialDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private final DnsServerAddresses addresses; + + UniSequentialDnsServerAddressStreamProvider(DnsServerAddresses addresses) { + this.addresses = ObjectUtil.checkNotNull(addresses, "addresses"); + } + + @Override + public final DnsServerAddressStream nameServerAddressStream(String hostname) { + return addresses.stream(); + } +} diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java index 29c65ed044..0a1a935e28 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java @@ -304,7 +304,7 @@ public class DnsNameResolverTest { builder.nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress())); } else { builder.nameServerProvider(new MultiDnsServerAddressStreamProvider(dnsServerAddressStreamProvider, - new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress()))); + new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress()))); } return builder; @@ -672,7 +672,7 @@ public class DnsNameResolverTest { observer.question.type() == CNAME || observer.question.type() == AAAA); } else if (o instanceof QueryWrittenEvent) { QueryFailedEvent failedEvent = (QueryFailedEvent) observer.events.poll(); - } else { + } else if (!(o instanceof QueryFailedEvent)) { fail("unexpected event type: " + o); } assertTrue(observer.events.isEmpty()); @@ -787,6 +787,121 @@ public class DnsNameResolverTest { } } + @Test(timeout = 5000) + public void secondDnsServerShouldBeUsedBeforeCNAMEFirstServerNotStarted() throws IOException { + secondDnsServerShouldBeUsedBeforeCNAME(false); + } + + @Test(timeout = 5000) + public void secondDnsServerShouldBeUsedBeforeCNAMEFirstServerFailResolve() throws IOException { + secondDnsServerShouldBeUsedBeforeCNAME(true); + } + + private static void secondDnsServerShouldBeUsedBeforeCNAME(boolean startDnsServer1) throws IOException { + final String knownHostName = "netty.io"; + final TestDnsServer dnsServer1 = new TestDnsServer(new HashSet(Arrays.asList("notnetty.com"))); + final TestDnsServer dnsServer2 = new TestDnsServer(new HashSet(Arrays.asList(knownHostName))); + DnsNameResolver resolver = null; + try { + final InetSocketAddress dnsServer1Address; + if (startDnsServer1) { + dnsServer1.start(); + dnsServer1Address = dnsServer1.localAddress(); + } else { + // Some address where a DNS server will not be running. + dnsServer1Address = new InetSocketAddress("127.0.0.1", 22); + } + dnsServer2.start(); + + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + new TestRecursiveCacheDnsQueryLifecycleObserverFactory(); + + DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next()) + .dnsQueryLifecycleObserverFactory(lifecycleObserverFactory) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .channelType(NioDatagramChannel.class) + .queryTimeoutMillis(1000) // We expect timeouts if startDnsServer1 is false + .optResourceEnabled(false); + + builder.nameServerProvider(new SequentialDnsServerAddressStreamProvider(dnsServer1Address, + dnsServer2.localAddress())); + resolver = builder.build(); + assertNotNull(resolver.resolve(knownHostName).syncUninterruptibly().getNow()); + + TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll(); + assertNotNull(observer); + assertEquals(1, lifecycleObserverFactory.observers.size()); + assertEquals(2, observer.events.size()); + QueryWrittenEvent writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer1Address, writtenEvent.dnsServerAddress); + QueryFailedEvent failedEvent = (QueryFailedEvent) observer.events.poll(); + + observer = lifecycleObserverFactory.observers.poll(); + assertEquals(2, observer.events.size()); + writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer2.localAddress(), writtenEvent.dnsServerAddress); + QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll(); + } finally { + if (resolver != null) { + resolver.close(); + } + dnsServer1.stop(); + dnsServer2.stop(); + } + } + + @Test(timeout = 5000) + public void aAndAAAAQueryShouldTryFirstDnsServerBeforeSecond() throws IOException { + final String knownHostName = "netty.io"; + final TestDnsServer dnsServer1 = new TestDnsServer(new HashSet(Arrays.asList("notnetty.com"))); + final TestDnsServer dnsServer2 = new TestDnsServer(new HashSet(Arrays.asList(knownHostName))); + DnsNameResolver resolver = null; + try { + dnsServer1.start(); + dnsServer2.start(); + + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + new TestRecursiveCacheDnsQueryLifecycleObserverFactory(); + + DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next()) + .resolvedAddressTypes(ResolvedAddressTypes.IPV6_PREFERRED) + .dnsQueryLifecycleObserverFactory(lifecycleObserverFactory) + .channelType(NioDatagramChannel.class) + .optResourceEnabled(false); + + builder.nameServerProvider(new SequentialDnsServerAddressStreamProvider(dnsServer1.localAddress(), + dnsServer2.localAddress())); + resolver = builder.build(); + assertNotNull(resolver.resolve(knownHostName).syncUninterruptibly().getNow()); + + TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll(); + assertNotNull(observer); + assertEquals(2, lifecycleObserverFactory.observers.size()); + assertEquals(2, observer.events.size()); + QueryWrittenEvent writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer1.localAddress(), writtenEvent.dnsServerAddress); + QueryFailedEvent failedEvent = (QueryFailedEvent) observer.events.poll(); + + observer = lifecycleObserverFactory.observers.poll(); + assertEquals(2, observer.events.size()); + writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer1.localAddress(), writtenEvent.dnsServerAddress); + failedEvent = (QueryFailedEvent) observer.events.poll(); + + observer = lifecycleObserverFactory.observers.poll(); + assertEquals(2, observer.events.size()); + writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer2.localAddress(), writtenEvent.dnsServerAddress); + QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll(); + } finally { + if (resolver != null) { + resolver.close(); + } + dnsServer1.stop(); + dnsServer2.stop(); + } + } + @Test public void testRecursiveResolveNoCache() throws Exception { testRecursiveResolveCache(false);