From 65ff68f1cdf7f74eee735bb51ad5bb1fcee9ba6d Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 29 Aug 2021 19:38:28 +0200 Subject: [PATCH] Use LookupSession for async lookups --- pom.xml | 2 +- .../com/spotify/dns/CachingLookupFactory.java | 108 +++++------ .../java/com/spotify/dns/DnsSrvResolver.java | 3 +- .../java/com/spotify/dns/DnsSrvResolvers.java | 8 +- .../java/com/spotify/dns/LookupFactory.java | 8 +- .../spotify/dns/MeteredDnsSrvResolver.java | 35 ++-- .../spotify/dns/RetainingDnsSrvResolver.java | 42 ++--- .../dns/ServiceResolvingChangeNotifier.java | 76 ++++---- .../com/spotify/dns/SimpleLookupFactory.java | 24 +-- .../com/spotify/dns/XBillDnsSrvResolver.java | 74 +++++--- .../spotify/dns/CachingLookupFactoryTest.java | 176 +++++++++--------- .../com/spotify/dns/DnsSrvResolversIT.java | 15 +- .../com/spotify/dns/DnsSrvWatchersTest.java | 9 +- .../dns/MeteredDnsSrvResolverTest.java | 29 +-- .../dns/RetainingDnsSrvResolverTest.java | 89 +++++---- .../ServiceResolvingChangeNotifierTest.java | 15 +- .../spotify/dns/SimpleLookupFactoryTest.java | 17 +- .../spotify/dns/XBillDnsSrvResolverTest.java | 55 +++--- .../com/spotify/dns/examples/BasicUsage.java | 18 +- 19 files changed, 408 insertions(+), 395 deletions(-) diff --git a/pom.xml b/pom.xml index 8bc0ff6..2e116b6 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ dnsjava dnsjava - 3.0.2 + 3.4.2-SNAPSHOT com.google.guava diff --git a/src/main/java/com/spotify/dns/CachingLookupFactory.java b/src/main/java/com/spotify/dns/CachingLookupFactory.java index 98656e1..f255fda 100644 --- a/src/main/java/com/spotify/dns/CachingLookupFactory.java +++ b/src/main/java/com/spotify/dns/CachingLookupFactory.java @@ -1,54 +1,54 @@ -/* - * Copyright (c) 2015 Spotify AB - * - * 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 com.spotify.dns; - -import static java.util.Objects.requireNonNull; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.util.concurrent.UncheckedExecutionException; -import java.util.concurrent.ExecutionException; -import org.xbill.DNS.Lookup; - -/** - * Caches Lookup instances using a per-thread cache; this is so that different threads will never - * get the same instance of Lookup. Lookup instances are not thread-safe. - */ -class CachingLookupFactory implements LookupFactory { - private final LookupFactory delegate; - private final ThreadLocal> cacheHolder; - - CachingLookupFactory(LookupFactory delegate) { - this.delegate = requireNonNull(delegate, "delegate"); - cacheHolder = - ThreadLocal.withInitial(() -> CacheBuilder.newBuilder().build()); - } - - @Override - public Lookup forName(final String fqdn) { - try { - return cacheHolder.get().get( - fqdn, - () -> delegate.forName(fqdn) - ); - } catch (ExecutionException e) { - throw new DnsException(e); - } catch (UncheckedExecutionException e) { - throw new DnsException(e); - } - } -} +///* +// * Copyright (c) 2015 Spotify AB +// * +// * 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 com.spotify.dns; +// +//import static java.util.Objects.requireNonNull; +// +//import com.google.common.cache.Cache; +//import com.google.common.cache.CacheBuilder; +//import com.google.common.util.concurrent.UncheckedExecutionException; +//import java.util.concurrent.ExecutionException; +//import org.xbill.DNS.Lookup; +// +///** +// * Caches Lookup instances using a per-thread cache; this is so that different threads will never +// * get the same instance of Lookup. Lookup instances are not thread-safe. +// */ +//class CachingLookupFactory implements LookupFactory { +// private final LookupFactory delegate; +// private final ThreadLocal> cacheHolder; +// +// CachingLookupFactory(LookupFactory delegate) { +// this.delegate = requireNonNull(delegate, "delegate"); +// cacheHolder = +// ThreadLocal.withInitial(() -> CacheBuilder.newBuilder().build()); +// } +// +// @Override +// public Lookup forName(final String fqdn) { +// try { +// return cacheHolder.get().get( +// fqdn, +// () -> delegate.forName(fqdn) +// ); +// } catch (ExecutionException e) { +// throw new DnsException(e); +// } catch (UncheckedExecutionException e) { +// throw new DnsException(e); +// } +// } +//} diff --git a/src/main/java/com/spotify/dns/DnsSrvResolver.java b/src/main/java/com/spotify/dns/DnsSrvResolver.java index 849d35d..3b8aa28 100644 --- a/src/main/java/com/spotify/dns/DnsSrvResolver.java +++ b/src/main/java/com/spotify/dns/DnsSrvResolver.java @@ -17,6 +17,7 @@ package com.spotify.dns; import java.util.List; +import java.util.concurrent.CompletionStage; /** * Contract for doing SRV lookups. @@ -30,5 +31,5 @@ public interface DnsSrvResolver { * @return a possibly empty list of matching records * @throws DnsException if there was an error doing the DNS lookup */ - List resolve(String fqdn); + CompletionStage> resolve(String fqdn); } diff --git a/src/main/java/com/spotify/dns/DnsSrvResolvers.java b/src/main/java/com/spotify/dns/DnsSrvResolvers.java index c23f77a..baff23e 100644 --- a/src/main/java/com/spotify/dns/DnsSrvResolvers.java +++ b/src/main/java/com/spotify/dns/DnsSrvResolvers.java @@ -79,7 +79,7 @@ public DnsSrvResolver build() { // or if that's empty, localhost. resolver = servers == null ? new ExtendedResolver() : - new ExtendedResolver(servers.toArray(new String[servers.size()])); + new ExtendedResolver(servers.toArray(new String[0])); } catch (UnknownHostException e) { throw new RuntimeException(e); } @@ -90,9 +90,9 @@ public DnsSrvResolver build() { LookupFactory lookupFactory = new SimpleLookupFactory(resolver); - if (cacheLookups) { - lookupFactory = new CachingLookupFactory(lookupFactory); - } +// if (cacheLookups) { +// lookupFactory = new CachingLookupFactory(lookupFactory); +// } DnsSrvResolver result = new XBillDnsSrvResolver(lookupFactory); diff --git a/src/main/java/com/spotify/dns/LookupFactory.java b/src/main/java/com/spotify/dns/LookupFactory.java index 946b24d..a170cb4 100644 --- a/src/main/java/com/spotify/dns/LookupFactory.java +++ b/src/main/java/com/spotify/dns/LookupFactory.java @@ -16,16 +16,16 @@ package com.spotify.dns; -import org.xbill.DNS.Lookup; +import org.xbill.DNS.lookup.LookupSession; /** - * Library-internal interface used for finding or creating {@link Lookup} instances. + * Library-internal interface used for finding or creating {@link LookupSession} instances. */ interface LookupFactory { /** - * Returns a {@link Lookup} instance capable of doing SRV lookups for the supplied FQDN. + * Returns a {@link LookupSession} instance capable of doing SRV lookups for the supplied FQDN. * @param fqdn the name to do lookups for * @return a Lookup instance */ - Lookup forName(String fqdn); + LookupSession forName(String fqdn); } diff --git a/src/main/java/com/spotify/dns/MeteredDnsSrvResolver.java b/src/main/java/com/spotify/dns/MeteredDnsSrvResolver.java index 6fcf6aa..a0ef27e 100644 --- a/src/main/java/com/spotify/dns/MeteredDnsSrvResolver.java +++ b/src/main/java/com/spotify/dns/MeteredDnsSrvResolver.java @@ -16,12 +16,14 @@ package com.spotify.dns; +import static com.google.common.base.Throwables.throwIfUnchecked; import static java.util.Objects.requireNonNull; import com.spotify.dns.statistics.DnsReporter; import com.spotify.dns.statistics.DnsTimingContext; import java.util.List; +import java.util.concurrent.CompletionStage; /** * Tracks metrics for DnsSrvResolver calls. @@ -36,27 +38,28 @@ class MeteredDnsSrvResolver implements DnsSrvResolver { } @Override - public List resolve(String fqdn) { + public CompletionStage> resolve(String fqdn) { // Only catch and report RuntimeException to avoid Error's since that would // most likely only aggravate any condition that causes them to be thrown. final DnsTimingContext resolveTimer = reporter.resolveTimer(); - final List result; + return delegate + .resolve(fqdn) + .handle( + (result, error) -> { + resolveTimer.stop(); + if (error == null) { + if (result.isEmpty()) { + reporter.reportEmpty(); + } - try { - result = delegate.resolve(fqdn); - } catch (RuntimeException error) { - reporter.reportFailure(error); - throw error; - } finally { - resolveTimer.stop(); - } - - if (result.isEmpty()) { - reporter.reportEmpty(); - } - - return result; + return result; + } else { + reporter.reportFailure(error); + throwIfUnchecked(error); + throw new RuntimeException(error); + } + }); } } diff --git a/src/main/java/com/spotify/dns/RetainingDnsSrvResolver.java b/src/main/java/com/spotify/dns/RetainingDnsSrvResolver.java index 653f3aa..f719539 100644 --- a/src/main/java/com/spotify/dns/RetainingDnsSrvResolver.java +++ b/src/main/java/com/spotify/dns/RetainingDnsSrvResolver.java @@ -23,6 +23,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.util.List; +import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeUnit; /** @@ -46,28 +47,27 @@ class RetainingDnsSrvResolver implements DnsSrvResolver { } @Override - public List resolve(final String fqdn) { + public CompletionStage> resolve(final String fqdn) { requireNonNull(fqdn, "fqdn"); - - try { - final List nodes = delegate.resolve(fqdn); - - // No nodes resolved? Return stale data. - if (nodes.isEmpty()) { - List cached = cache.getIfPresent(fqdn); - return (cached != null) ? cached : nodes; - } - - cache.put(fqdn, nodes); - - return nodes; - } catch (Exception e) { - if (cache.getIfPresent(fqdn) != null) { - return cache.getIfPresent(fqdn); + return delegate.resolve(fqdn).handle((nodes, e) -> { + if (e == null){ + // No nodes resolved? Return stale data. + if (nodes.isEmpty()) { + List cached = cache.getIfPresent(fqdn); + return (cached != null) ? cached : nodes; + } + + cache.put(fqdn, nodes); + + return nodes; + } else{ + if (cache.getIfPresent(fqdn) != null) { + return cache.getIfPresent(fqdn); + } + + throwIfUnchecked(e); + throw new RuntimeException(e); } - - throwIfUnchecked(e); - throw new RuntimeException(e); - } + }); } } diff --git a/src/main/java/com/spotify/dns/ServiceResolvingChangeNotifier.java b/src/main/java/com/spotify/dns/ServiceResolvingChangeNotifier.java index a91d4a4..021777c 100644 --- a/src/main/java/com/spotify/dns/ServiceResolvingChangeNotifier.java +++ b/src/main/java/com/spotify/dns/ServiceResolvingChangeNotifier.java @@ -19,7 +19,6 @@ import static java.util.Objects.requireNonNull; import com.google.common.collect.ImmutableSet; -import java.util.List; import java.util.Set; import java.util.function.Function; import org.slf4j.Logger; @@ -53,7 +52,7 @@ class ServiceResolvingChangeNotifier extends AbstractChangeNotifier * and put into a set. The set will then be compared to the previous set and if a * change is detected, the notifier will fire. * - *

An optional {@link ErrorHandler} can be used to reacto on {@link DnsException}s thrown + *

An optional {@link ErrorHandler} can be used to react on {@link DnsException}s thrown * by the {@link DnsSrvResolver}. * * @param resolver The resolver to use. @@ -88,45 +87,42 @@ public void run() { return; } - final List nodes; - try { - nodes = resolver.resolve(fqdn); - } catch (DnsException e) { - if (errorHandler != null) { - errorHandler.handle(fqdn, e); + resolver.resolve(fqdn).whenComplete((nodes, e) -> { + if (e instanceof DnsException) { + if (errorHandler != null) { + errorHandler.handle(fqdn, (DnsException) e); + } + log.error(e.getMessage(), e); + fireIfFirstError(); + } else if (e != null) { + log.error(e.getMessage(), e); + fireIfFirstError(); + } else { + final Set current; + try { + ImmutableSet.Builder builder = ImmutableSet.builder(); + for (LookupResult node : nodes) { + T transformed = resultTransformer.apply(node); + builder.add(requireNonNull(transformed, "transformed")); + } + current = builder.build(); + } catch (Exception transformerException) { + log.error(transformerException.getMessage(), transformerException); + fireIfFirstError(); + return; + } + + if (ChangeNotifiers.isNoLongerInitial(current, records) || !current.equals(records)) { + // This means that any subsequent DNS error will be ignored and the existing result will be kept + waitingForFirstEvent = false; + final ChangeNotification changeNotification = + newChangeNotification(current, records); + records = current; + + fireRecordsUpdated(changeNotification); + } } - log.error(e.getMessage(), e); - fireIfFirstError(); - return; - } catch (Exception e) { - log.error(e.getMessage(), e); - fireIfFirstError(); - return; - } - - final Set current; - try { - ImmutableSet.Builder builder = ImmutableSet.builder(); - for (LookupResult node : nodes) { - T transformed = resultTransformer.apply(node); - builder.add(requireNonNull(transformed, "transformed")); - } - current = builder.build(); - } catch (Exception e) { - log.error(e.getMessage(), e); - fireIfFirstError(); - return; - } - - if (ChangeNotifiers.isNoLongerInitial(current, records) || !current.equals(records)) { - // This means that any subsequent DNS error will be ignored and the existing result will be kept - waitingForFirstEvent = false; - final ChangeNotification changeNotification = - newChangeNotification(current, records); - records = current; - - fireRecordsUpdated(changeNotification); - } + }); } private void fireIfFirstError() { diff --git a/src/main/java/com/spotify/dns/SimpleLookupFactory.java b/src/main/java/com/spotify/dns/SimpleLookupFactory.java index 3954f02..9d72100 100644 --- a/src/main/java/com/spotify/dns/SimpleLookupFactory.java +++ b/src/main/java/com/spotify/dns/SimpleLookupFactory.java @@ -21,32 +21,22 @@ import org.xbill.DNS.Resolver; import org.xbill.DNS.TextParseException; import org.xbill.DNS.Type; +import org.xbill.DNS.lookup.LookupSession; -/** - * A LookupFactory that always returns new instances. - */ +/** A LookupFactory that always returns new instances. */ public class SimpleLookupFactory implements LookupFactory { - - private final Resolver resolver; + private final LookupSession session; public SimpleLookupFactory() { - this(null); + this(Lookup.getDefaultResolver()); } public SimpleLookupFactory(Resolver resolver) { - this.resolver = resolver; + session = LookupSession.builder().resolver(resolver).build(); } @Override - public Lookup forName(String fqdn) { - try { - final Lookup lookup = new Lookup(fqdn, Type.SRV, DClass.IN); - if (resolver != null) { - lookup.setResolver(resolver); - } - return lookup; - } catch (TextParseException e) { - throw new DnsException("unable to create lookup for name: " + fqdn, e); - } + public LookupSession forName(String fqdn) { + return session; } } diff --git a/src/main/java/com/spotify/dns/XBillDnsSrvResolver.java b/src/main/java/com/spotify/dns/XBillDnsSrvResolver.java index ccc5f83..0fcb194 100644 --- a/src/main/java/com/spotify/dns/XBillDnsSrvResolver.java +++ b/src/main/java/com/spotify/dns/XBillDnsSrvResolver.java @@ -22,15 +22,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.xbill.DNS.Lookup; +import org.xbill.DNS.DClass; +import org.xbill.DNS.Name; import org.xbill.DNS.Record; import org.xbill.DNS.SRVRecord; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; +import org.xbill.DNS.lookup.LookupSession; +import org.xbill.DNS.lookup.NoSuchDomainException; +import org.xbill.DNS.lookup.NoSuchRRSetException; import java.util.List; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; /** - * A DnsSrvResolver implementation that uses the dnsjava implementation from xbill.org: - * http://www.xbill.org/dnsjava/ + * A DnsSrvResolver implementation that uses the dnsjava implementation: + * https://github.com/dnsjava/dnsjava */ class XBillDnsSrvResolver implements DnsSrvResolver { private static final Logger LOG = LoggerFactory.getLogger(XBillDnsSrvResolver.class); @@ -42,39 +50,45 @@ class XBillDnsSrvResolver implements DnsSrvResolver { } @Override - public List resolve(final String fqdn) { - Lookup lookup = lookupFactory.forName(fqdn); - Record[] queryResult = lookup.run(); + public CompletionStage> resolve(final String fqdn) { + LookupSession lookup = lookupFactory.forName(fqdn); + Name name; + try { + name = Name.fromString(fqdn); + } catch (TextParseException e) { + throw new DnsException("unable to create lookup for name: " + fqdn, e); + } - switch (lookup.getResult()) { - case Lookup.SUCCESSFUL: - return toLookupResults(queryResult); - case Lookup.HOST_NOT_FOUND: - // fallthrough - case Lookup.TYPE_NOT_FOUND: - LOG.warn("No results returned for query '{}'; result from XBill: {} - {}", - fqdn, lookup.getResult(), lookup.getErrorString()); - return ImmutableList.of(); - default: + return lookup.lookupAsync(name, Type.SRV, DClass.IN).handle((result, ex) ->{ + if (ex == null){ + return toLookupResults(result); + } else{ + Throwable cause = ex; + if (ex instanceof CompletionException && ex.getCause() != null) { + cause = ex.getCause(); + } + if (cause instanceof NoSuchRRSetException || cause instanceof NoSuchDomainException) { + LOG.warn("No results returned for query '{}'; result from dnsjava: {}", + fqdn, ex.getMessage()); + return ImmutableList.of(); + } throw new DnsException( - String.format("Lookup of '%s' failed with code: %d - %s ", - fqdn, lookup.getResult(), lookup.getErrorString())); - } + String.format("Lookup of '%s' failed: %s ", fqdn, ex.getMessage()), ex); + } + }); } - private static List toLookupResults(Record[] queryResult) { + private static List toLookupResults(org.xbill.DNS.lookup.LookupResult queryResult) { ImmutableList.Builder builder = ImmutableList.builder(); - if (queryResult != null) { - for (Record record: queryResult) { - if (record instanceof SRVRecord) { - SRVRecord srvRecord = (SRVRecord) record; - builder.add(LookupResult.create(srvRecord.getTarget().toString(), - srvRecord.getPort(), - srvRecord.getPriority(), - srvRecord.getWeight(), - srvRecord.getTTL())); - } + for (Record record: queryResult.getRecords()) { + if (record instanceof SRVRecord) { + SRVRecord srvRecord = (SRVRecord) record; + builder.add(LookupResult.create(srvRecord.getTarget().toString(), + srvRecord.getPort(), + srvRecord.getPriority(), + srvRecord.getWeight(), + srvRecord.getTTL())); } } diff --git a/src/test/java/com/spotify/dns/CachingLookupFactoryTest.java b/src/test/java/com/spotify/dns/CachingLookupFactoryTest.java index 478d9fa..f9f0c1a 100644 --- a/src/test/java/com/spotify/dns/CachingLookupFactoryTest.java +++ b/src/test/java/com/spotify/dns/CachingLookupFactoryTest.java @@ -1,88 +1,88 @@ -/* - * Copyright (c) 2015 Spotify AB - * - * 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 com.spotify.dns; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.junit.Before; -import org.junit.Test; -import org.xbill.DNS.Lookup; - -public class CachingLookupFactoryTest { - CachingLookupFactory factory; - - LookupFactory delegate; - - Lookup lookup; - Lookup lookup2; - - @Before - public void setUp() throws Exception { - delegate = mock(LookupFactory.class); - - factory = new CachingLookupFactory(delegate); - - lookup = new Lookup("hi"); - lookup2 = new Lookup("hey"); - } - - @Test - public void shouldReturnResultsFromDelegate() { - when(delegate.forName("a name")).thenReturn(lookup); - - assertThat(factory.forName("a name"), equalTo(lookup)); - } - - @Test - public void shouldCacheResultsForSubsequentQueries() { - when(delegate.forName("hej")).thenReturn(lookup, lookup2); - - Lookup first = factory.forName("hej"); - Lookup second = factory.forName("hej"); - - assertThat(first == second, is(true)); - } - - @Test - public void shouldReturnDifferentForDifferentQueries() { - when(delegate.forName("hej")).thenReturn(lookup); - when(delegate.forName("hopp")).thenReturn(lookup2); - - Lookup first = factory.forName("hej"); - Lookup second = factory.forName("hopp"); - - assertThat(first == second, is(false)); - } - - @Test - public void shouldReturnDifferentForDifferentThreads() throws Exception { - ExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); - factory = new CachingLookupFactory(new SimpleLookupFactory()); - - Lookup first = factory.forName("hej"); - Lookup second = executorService.submit(() -> factory.forName("hej")).get(); - - assertThat(second, not(equalTo(first))); - } -} +///* +// * Copyright (c) 2015 Spotify AB +// * +// * 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 com.spotify.dns; +// +//import static org.hamcrest.CoreMatchers.equalTo; +//import static org.hamcrest.CoreMatchers.is; +//import static org.hamcrest.CoreMatchers.not; +//import static org.junit.Assert.assertThat; +//import static org.mockito.Mockito.mock; +//import static org.mockito.Mockito.when; +// +//import java.util.concurrent.ExecutorService; +//import java.util.concurrent.Executors; +//import org.junit.Before; +//import org.junit.Test; +//import org.xbill.DNS.Lookup; +// +//public class CachingLookupFactoryTest { +// CachingLookupFactory factory; +// +// LookupFactory delegate; +// +// Lookup lookup; +// Lookup lookup2; +// +// @Before +// public void setUp() throws Exception { +// delegate = mock(LookupFactory.class); +// +// factory = new CachingLookupFactory(delegate); +// +// lookup = new Lookup("hi"); +// lookup2 = new Lookup("hey"); +// } +// +// @Test +// public void shouldReturnResultsFromDelegate() { +// when(delegate.forName("a name")).thenReturn(lookup); +// +// assertThat(factory.forName("a name"), equalTo(lookup)); +// } +// +// @Test +// public void shouldCacheResultsForSubsequentQueries() { +// when(delegate.forName("hej")).thenReturn(lookup, lookup2); +// +// Lookup first = factory.forName("hej"); +// Lookup second = factory.forName("hej"); +// +// assertThat(first == second, is(true)); +// } +// +// @Test +// public void shouldReturnDifferentForDifferentQueries() { +// when(delegate.forName("hej")).thenReturn(lookup); +// when(delegate.forName("hopp")).thenReturn(lookup2); +// +// Lookup first = factory.forName("hej"); +// Lookup second = factory.forName("hopp"); +// +// assertThat(first == second, is(false)); +// } +// +// @Test +// public void shouldReturnDifferentForDifferentThreads() throws Exception { +// ExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); +// factory = new CachingLookupFactory(new SimpleLookupFactory()); +// +// Lookup first = factory.forName("hej"); +// Lookup second = executorService.submit(() -> factory.forName("hej")).get(); +// +// assertThat(second, not(equalTo(first))); +// } +//} diff --git a/src/test/java/com/spotify/dns/DnsSrvResolversIT.java b/src/test/java/com/spotify/dns/DnsSrvResolversIT.java index aa00966..535e2c3 100644 --- a/src/test/java/com/spotify/dns/DnsSrvResolversIT.java +++ b/src/test/java/com/spotify/dns/DnsSrvResolversIT.java @@ -35,6 +35,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.hamcrest.Matchers; import org.junit.Before; @@ -54,8 +55,8 @@ public void setUp() { } @Test - public void shouldReturnResultsForValidQuery() { - assertThat(resolver.resolve("_spotify-client._tcp.spotify.com").isEmpty(), is(false)); + public void shouldReturnResultsForValidQuery() throws ExecutionException, InterruptedException { + assertThat(resolver.resolve("_spotify-client._tcp.spotify.com").toCompletableFuture().get().isEmpty(), is(false)); } @Test @@ -79,7 +80,7 @@ public void testCorrectSequenceOfNotifications() { } @Test - public void shouldTrackMetricsWhenToldTo() { + public void shouldTrackMetricsWhenToldTo() throws ExecutionException, InterruptedException { final DnsReporter reporter = mock(DnsReporter.class); final DnsTimingContext timingReporter = mock(DnsTimingContext.class); @@ -88,16 +89,16 @@ public void shouldTrackMetricsWhenToldTo() { .build(); when(reporter.resolveTimer()).thenReturn(timingReporter); - resolver.resolve("_spotify-client._tcp.sto.spotify.net"); + resolver.resolve("_spotify-client._tcp.sto.spotify.net").toCompletableFuture().get(); verify(timingReporter).stop(); verify(reporter, never()).reportFailure(isA(RuntimeException.class)); verify(reporter, times(1)).reportEmpty(); } @Test - public void shouldFailForBadHostNames() { + public void shouldFailForBadHostNames() throws Exception { try { - resolver.resolve("nonexistenthost"); + resolver.resolve("nonexistenthost").toCompletableFuture().get(); } catch (DnsException e) { assertThat(e.getMessage(), containsString("host not found")); @@ -111,7 +112,7 @@ public void shouldReturnResultsUsingSpecifiedServers() throws Exception { .newBuilder() .servers(List.of(server)) .build(); - assertThat(resolver.resolve("_spotify-client._tcp.spotify.com").isEmpty(), is(false)); + assertThat(resolver.resolve("_spotify-client._tcp.spotify.com").toCompletableFuture().get().isEmpty(), is(false)); } @Test diff --git a/src/test/java/com/spotify/dns/DnsSrvWatchersTest.java b/src/test/java/com/spotify/dns/DnsSrvWatchersTest.java index d4a440e..00530f6 100644 --- a/src/test/java/com/spotify/dns/DnsSrvWatchersTest.java +++ b/src/test/java/com/spotify/dns/DnsSrvWatchersTest.java @@ -6,10 +6,13 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; +import org.xbill.DNS.lookup.NoSuchDomainException; public class DnsSrvWatchersTest { @@ -75,11 +78,11 @@ public FakeResolver(String fqdn, LookupResult result) { } @Override - public List resolve(String fqdn) { + public CompletionStage> resolve(String fqdn) { if (this.fqdn.equals(fqdn)) { - return List.of(result); + return CompletableFuture.completedFuture(List.of(result)); } else { - return null; + return CompletableFuture.failedFuture(new DnsException(this.fqdn + " != " + fqdn)); } } } diff --git a/src/test/java/com/spotify/dns/MeteredDnsSrvResolverTest.java b/src/test/java/com/spotify/dns/MeteredDnsSrvResolverTest.java index 4986201..b3f544c 100644 --- a/src/test/java/com/spotify/dns/MeteredDnsSrvResolverTest.java +++ b/src/test/java/com/spotify/dns/MeteredDnsSrvResolverTest.java @@ -26,6 +26,9 @@ import com.spotify.dns.statistics.DnsReporter; import com.spotify.dns.statistics.DnsTimingContext; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -71,9 +74,10 @@ public void after() { @Test public void shouldCountSuccessful() throws Exception { - when(delegate.resolve(FQDN)).thenReturn(NOT_EMPTY); + CompletableFuture> completedNotEmpty = CompletableFuture.completedFuture(NOT_EMPTY); + when(delegate.resolve(FQDN)).thenReturn(completedNotEmpty); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); verify(reporter, never()).reportEmpty(); verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); @@ -81,9 +85,10 @@ public void shouldCountSuccessful() throws Exception { @Test public void shouldReportEmpty() throws Exception { - when(delegate.resolve(FQDN)).thenReturn(EMPTY); + CompletableFuture> completedEmpty = CompletableFuture.completedFuture(EMPTY); + when(delegate.resolve(FQDN)).thenReturn(completedEmpty); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); verify(reporter).reportEmpty(); verify(reporter, never()).reportFailure(RUNTIME_EXCEPTION); @@ -91,13 +96,13 @@ public void shouldReportEmpty() throws Exception { @Test public void shouldReportRuntimeException() throws Exception { - when(delegate.resolve(FQDN)).thenThrow(RUNTIME_EXCEPTION); + when(delegate.resolve(FQDN)).thenReturn(CompletableFuture.failedFuture((RUNTIME_EXCEPTION))); try { - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); fail("resolve should have thrown exception"); - } catch(RuntimeException e) { - assertEquals(RUNTIME_EXCEPTION, e); + } catch(ExecutionException e) { + assertEquals(RUNTIME_EXCEPTION, e.getCause()); } verify(reporter, never()).reportEmpty(); @@ -106,13 +111,13 @@ public void shouldReportRuntimeException() throws Exception { @Test public void shouldNotReportError() throws Exception { - when(delegate.resolve(FQDN)).thenThrow(ERROR); + when(delegate.resolve(FQDN)).thenReturn(CompletableFuture.failedFuture(ERROR)); try { - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); fail("resolve should have thrown exception"); - } catch(Error e) { - assertEquals(ERROR, e); + } catch(ExecutionException e) { + assertEquals(ERROR, e.getCause()); } verify(reporter, never()).reportEmpty(); diff --git a/src/test/java/com/spotify/dns/RetainingDnsSrvResolverTest.java b/src/test/java/com/spotify/dns/RetainingDnsSrvResolverTest.java index 0f0ef59..8eee1de 100644 --- a/src/test/java/com/spotify/dns/RetainingDnsSrvResolverTest.java +++ b/src/test/java/com/spotify/dns/RetainingDnsSrvResolverTest.java @@ -24,6 +24,9 @@ import static org.mockito.Mockito.when; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -54,101 +57,107 @@ public void setUp() { } @Test - public void shouldReturnResultsFromDelegate() { - when(delegate.resolve(FQDN)).thenReturn(nodes1); + public void shouldReturnResultsFromDelegate() throws ExecutionException, InterruptedException { + when(delegate.resolve(FQDN)).thenReturn(CompletableFuture.completedFuture(nodes1)); - assertThat(resolver.resolve(FQDN), equalTo(nodes1)); + assertThat(resolver.resolve(FQDN).toCompletableFuture().get(), equalTo(nodes1)); } @Test - public void shouldReturnResultsFromDelegateEachTime() { - when(delegate.resolve(FQDN)).thenReturn(nodes1).thenReturn(nodes2); + public void shouldReturnResultsFromDelegateEachTime() throws ExecutionException, InterruptedException { + when(delegate.resolve(FQDN)) + .thenReturn(CompletableFuture.completedFuture(nodes1)) + .thenReturn(CompletableFuture.completedFuture(nodes2)); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); - assertThat(resolver.resolve(FQDN), equalTo(nodes2)); + assertThat(resolver.resolve(FQDN).toCompletableFuture().get(), equalTo(nodes2)); } @Test - public void shouldRetainDataIfNewResultEmpty() { - when(delegate.resolve(FQDN)).thenReturn(nodes1).thenReturn(nodes()); + public void shouldRetainDataIfNewResultEmpty() throws ExecutionException, InterruptedException { + when(delegate.resolve(FQDN)) + .thenReturn(CompletableFuture.completedFuture(nodes1)) + .thenReturn(CompletableFuture.completedFuture(nodes())); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); - assertThat(resolver.resolve(FQDN), equalTo(nodes1)); + assertThat(resolver.resolve(FQDN).toCompletableFuture().get(), equalTo(nodes1)); } @Test - public void shouldRetainDataOnFailure() { + public void shouldRetainDataOnFailure() throws ExecutionException, InterruptedException { when(delegate.resolve(FQDN)) - .thenReturn(nodes1) - .thenThrow(new DnsException("expected")); + .thenReturn(CompletableFuture.completedFuture(nodes1)) + .thenReturn(CompletableFuture.failedFuture(new DnsException("expected"))); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); - assertThat(resolver.resolve(FQDN), equalTo(nodes1)); + assertThat(resolver.resolve(FQDN).toCompletableFuture().get(), equalTo(nodes1)); } @Test - public void shouldThrowOnFailureAndNoDataAvailable() { - when(delegate.resolve(FQDN)).thenThrow(new DnsException("expected")); + public void shouldThrowOnFailureAndNoDataAvailable() throws ExecutionException, InterruptedException { + DnsException cause = new DnsException("expected"); + when(delegate.resolve(FQDN)).thenReturn(CompletableFuture.failedFuture(cause)); - thrown.expect(DnsException.class); - thrown.expectMessage("expected"); + thrown.expect(ExecutionException.class); + thrown.expectCause(is(cause)); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); } @Test - public void shouldReturnEmptyOnEmptyAndNoDataAvailable() { - when(delegate.resolve(FQDN)).thenReturn(nodes()); + public void shouldReturnEmptyOnEmptyAndNoDataAvailable() throws ExecutionException, InterruptedException { + when(delegate.resolve(FQDN)).thenReturn(CompletableFuture.completedFuture(nodes())); - assertThat(resolver.resolve(FQDN).isEmpty(), is(true)); + assertThat(resolver.resolve(FQDN).toCompletableFuture().get().isEmpty(), is(true)); } @Test - public void shouldNotStoreEmptyResults() { + public void shouldNotStoreEmptyResults() throws ExecutionException, InterruptedException { + DnsException cause = new DnsException("expected"); when(delegate.resolve(FQDN)) - .thenReturn(nodes()) - .thenThrow(new DnsException("expected")); + .thenReturn(CompletableFuture.completedFuture(nodes())) + .thenReturn(CompletableFuture.failedFuture(cause)); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); - thrown.expect(DnsException.class); - thrown.expectMessage("expected"); + thrown.expect(ExecutionException.class); + thrown.expectCause(is(cause)); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); } @Test public void shouldNotRetainPastEndOfRetentionOnEmptyResults() throws Exception { when(delegate.resolve(FQDN)) - .thenReturn(nodes("aresult")) - .thenReturn(nodes()); + .thenReturn(CompletableFuture.completedFuture(nodes("aresult"))) + .thenReturn(CompletableFuture.completedFuture(nodes())); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); // expire retained entry Thread.sleep(RETENTION_TIME_MILLIS); - assertThat(resolver.resolve(FQDN).isEmpty(), is(true)); + assertThat(resolver.resolve(FQDN).toCompletableFuture().get().isEmpty(), is(true)); } @Test public void shouldNotRetainPastEndOfRetentionOnException() throws Exception { DnsException expected = new DnsException("expected"); when(delegate.resolve(FQDN)) - .thenReturn(nodes("aresult")) - .thenThrow(expected); + .thenReturn(CompletableFuture.completedFuture(nodes("aresult"))) + .thenReturn(CompletableFuture.failedFuture(expected)); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); // expire retained entry Thread.sleep(RETENTION_TIME_MILLIS); - thrown.expect(equalTo(expected)); + thrown.expectCause(equalTo(expected)); - resolver.resolve(FQDN); + resolver.resolve(FQDN).toCompletableFuture().get(); } @Test diff --git a/src/test/java/com/spotify/dns/ServiceResolvingChangeNotifierTest.java b/src/test/java/com/spotify/dns/ServiceResolvingChangeNotifierTest.java index 2044c45..80ce6b1 100644 --- a/src/test/java/com/spotify/dns/ServiceResolvingChangeNotifierTest.java +++ b/src/test/java/com/spotify/dns/ServiceResolvingChangeNotifierTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.when; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import org.junit.Before; import org.junit.Test; @@ -62,7 +63,7 @@ public void shouldCallListenerOnChange() { LookupResult result1 = result("host", 1234); LookupResult result2 = result("host", 4321); when(resolver.resolve(FQDN)) - .thenReturn(of(result1), of(result1, result2)); + .thenReturn(CompletableFuture.completedFuture(of(result1)), CompletableFuture.completedFuture(of(result1, result2))); sut.run(); sut.run(); @@ -94,7 +95,7 @@ public void shouldCallListenerOnSet() { LookupResult result = result("host", 1234); when(resolver.resolve(FQDN)) - .thenReturn(of(result)); + .thenReturn(CompletableFuture.completedFuture(of(result))); sut.run(); sut.setListener(listener, true); @@ -118,7 +119,7 @@ public void shouldReturnImmutableSets() { LookupResult result1 = result("host", 1234); LookupResult result2 = result("host", 4321); when(resolver.resolve(FQDN)) - .thenReturn(of(result1), of(result1, result2)); + .thenReturn(CompletableFuture.completedFuture(of(result1)), CompletableFuture.completedFuture(of(result1, result2))); sut.run(); sut.setListener(listener, true); @@ -152,7 +153,7 @@ public void shouldOnlyChangeIfTransformedValuesChange() { LookupResult result1 = result("host", 1234); LookupResult result2 = result("host", 4321); when(resolver.resolve(FQDN)) - .thenReturn(of(result1), of(result1, result2)); + .thenReturn(CompletableFuture.completedFuture(of(result1)), CompletableFuture.completedFuture(of(result1, result2))); sut.run(); sut.run(); @@ -189,10 +190,10 @@ public void shouldDoSomethingWithNulls() { ChangeNotifier.Listener listener = mock(ChangeNotifier.Listener.class); when(resolver.resolve(FQDN)) - .thenReturn(of( + .thenReturn(CompletableFuture.completedFuture(of( result("host1", 1234), result("host2", 1234), - result("host3", 1234))); + result("host3", 1234)))); when(f.apply(any(LookupResult.class))) .thenReturn("foo", null, "bar"); @@ -213,7 +214,7 @@ public void shouldCallErrorHandlerOnResolveErrors() { DnsException exception = new DnsException("something wrong"); when(resolver.resolve(FQDN)) - .thenThrow(exception); + .thenReturn(CompletableFuture.failedFuture(exception)); sut.setListener(listener, false); sut.run(); diff --git a/src/test/java/com/spotify/dns/SimpleLookupFactoryTest.java b/src/test/java/com/spotify/dns/SimpleLookupFactoryTest.java index 1630e3a..a8809c8 100644 --- a/src/test/java/com/spotify/dns/SimpleLookupFactoryTest.java +++ b/src/test/java/com/spotify/dns/SimpleLookupFactoryTest.java @@ -27,6 +27,7 @@ import org.junit.rules.ExpectedException; import org.xbill.DNS.Lookup; import org.xbill.DNS.TextParseException; +import org.xbill.DNS.lookup.LookupSession; public class SimpleLookupFactoryTest { @@ -46,18 +47,10 @@ public void shouldCreateLookups() { } @Test - public void shouldCreateNewLookupsEachTime() { - Lookup first = factory.forName("some.other.name."); - Lookup second = factory.forName("some.other.name."); + public void shouldNotCreateNewLookupsEachTime() { + LookupSession first = factory.forName("some.other.name."); + LookupSession second = factory.forName("some.other.name."); - assertThat(first == second, is(false)); - } - - @Test - public void shouldRethrowXBillExceptions() { - thrown.expect(DnsException.class); - thrown.expectCause(isA(TextParseException.class)); - - factory.forName("bad\\1 name"); + assertThat(first == second, is(true)); } } diff --git a/src/test/java/com/spotify/dns/XBillDnsSrvResolverTest.java b/src/test/java/com/spotify/dns/XBillDnsSrvResolverTest.java index ad17540..8db2ec7 100644 --- a/src/test/java/com/spotify/dns/XBillDnsSrvResolverTest.java +++ b/src/test/java/com/spotify/dns/XBillDnsSrvResolverTest.java @@ -18,7 +18,9 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -26,6 +28,8 @@ import java.io.IOException; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import org.junit.After; import org.junit.Before; @@ -43,6 +47,9 @@ import org.xbill.DNS.Section; import org.xbill.DNS.TextParseException; import org.xbill.DNS.Type; +import org.xbill.DNS.WireParseException; +import org.xbill.DNS.lookup.LookupFailedException; +import org.xbill.DNS.lookup.LookupSession; public class XBillDnsSrvResolverTest { XBillDnsSrvResolver resolver; @@ -73,7 +80,7 @@ public void shouldReturnResultsFromLookup() throws Exception { setupResponseForQuery(fqdn, fqdn, resultNodes); - List actual = resolver.resolve(fqdn); + List actual = resolver.resolve(fqdn).toCompletableFuture().get(); Set nodeNames = actual.stream().map(LookupResult::host).collect(Collectors.toSet()); @@ -82,34 +89,25 @@ public void shouldReturnResultsFromLookup() throws Exception { @Test public void shouldIndicateCauseFromXBillIfLookupFails() throws Exception { - thrown.expect(DnsException.class); - thrown.expectMessage("response does not match query"); - String fqdn = "thefqdn."; setupResponseForQuery(fqdn, "somethingelse.", "node1.domain.", "node2.domain."); - resolver.resolve(fqdn); - } - - @Test - public void shouldIndicateNameIfLookupFails() throws Exception { - thrown.expect(DnsException.class); - thrown.expectMessage("thefqdn."); - - String fqdn = "thefqdn."; - setupResponseForQuery(fqdn, "somethingelse.", "node1.domain.", "node2.domain."); - - resolver.resolve(fqdn); + try{ + resolver.resolve(fqdn).toCompletableFuture().get(); + fail("expected lookup failure"); + } catch (ExecutionException ex){ + assertThat(ex.getCause().getMessage(), containsString("Lookup of 'thefqdn.'")); + } } @Test public void shouldReturnEmptyForHostNotFound() throws Exception { String fqdn = "thefqdn."; - when(lookupFactory.forName(fqdn)).thenReturn(testLookup(fqdn)); - when(xbillResolver.send(any(Message.class))).thenReturn(messageWithRCode(fqdn, Rcode.NXDOMAIN)); + when(lookupFactory.forName(fqdn)).thenReturn(testLookup()); + when(xbillResolver.sendAsync(any(Message.class))).thenReturn(CompletableFuture.completedFuture(messageWithRCode(fqdn, Rcode.NXDOMAIN))); - assertThat(resolver.resolve(fqdn).isEmpty(), is(true)); + assertThat(resolver.resolve(fqdn).toCompletableFuture().get().isEmpty(), is(true)); } // not testing for type not found, as I don't know how to set that up... @@ -129,17 +127,18 @@ private Message messageWithRCode(String query, int rcode) throws TextParseExcept private void setupResponseForQuery(String queryFqdn, String responseFqdn, String... results) throws IOException { - when(lookupFactory.forName(queryFqdn)).thenReturn(testLookup(queryFqdn)); - when(xbillResolver.send(any(Message.class))) - .thenReturn(messageWithNodes(responseFqdn, results)); + when(lookupFactory.forName(queryFqdn)).thenReturn(testLookup()); + if (queryFqdn.equals(responseFqdn)) { + when(xbillResolver.sendAsync(any(Message.class))) + .thenReturn(CompletableFuture.completedFuture(messageWithNodes(responseFqdn, results))); + } else { + when(xbillResolver.sendAsync(any(Message.class))) + .thenReturn(CompletableFuture.failedFuture(new WireParseException("invalid name in message: "))); + } } - private Lookup testLookup(String thefqdn) throws TextParseException { - Lookup result = new Lookup(thefqdn, Type.SRV); - - result.setResolver(xbillResolver); - - return result; + private LookupSession testLookup() { + return LookupSession.builder().resolver(xbillResolver).build(); } private Message messageWithNodes(String query, String[] names) throws TextParseException { diff --git a/src/test/java/com/spotify/dns/examples/BasicUsage.java b/src/test/java/com/spotify/dns/examples/BasicUsage.java index 07636cc..e618fdc 100644 --- a/src/test/java/com/spotify/dns/examples/BasicUsage.java +++ b/src/test/java/com/spotify/dns/examples/BasicUsage.java @@ -16,7 +16,6 @@ package com.spotify.dns.examples; -import com.spotify.dns.DnsException; import com.spotify.dns.DnsSrvResolver; import com.spotify.dns.DnsSrvResolvers; import com.spotify.dns.LookupResult; @@ -25,7 +24,6 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.util.List; public final class BasicUsage { @@ -49,15 +47,15 @@ public static void main(String[] args) throws IOException { if (line == null || line.isEmpty()) { quit = true; } else { - try { - List nodes = resolver.resolve(line); - - for (LookupResult node : nodes) { - System.out.println(node); + resolver.resolve(line).whenComplete((nodes, e) -> { + if (e == null) { + for (LookupResult node : nodes) { + System.out.println(node); + } + } else { + e.printStackTrace(System.out); } - } catch (DnsException e) { - e.printStackTrace(System.out); - } + }); } } }