blob: 181b210197d19842c6949bc13ab2546ceb9adbf6 [file] [log] [blame]
/*
* Copyright 2015, 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 static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Iterables;
import io.grpc.Attributes;
import io.grpc.EquivalentAddressGroup;
import io.grpc.NameResolver;
import io.grpc.Status;
import io.grpc.internal.DnsNameResolver.DelegateResolver;
import io.grpc.internal.DnsNameResolver.ResolutionResults;
import io.grpc.internal.SharedResourceHolder.Resource;
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.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/** Unit tests for {@link DnsNameResolver}. */
@RunWith(JUnit4.class)
public class DnsNameResolverTest {
private static final int DEFAULT_PORT = 887;
private static final Attributes NAME_RESOLVER_PARAMS =
Attributes.newBuilder().set(NameResolver.Factory.PARAMS_DEFAULT_PORT, DEFAULT_PORT).build();
private final DnsNameResolverProvider provider = new DnsNameResolverProvider();
private final FakeClock fakeClock = new FakeClock();
private final FakeClock fakeExecutor = new FakeClock();
private MockResolver mockResolver = new MockResolver();
private final Resource<ScheduledExecutorService> fakeTimerServiceResource =
new Resource<ScheduledExecutorService>() {
@Override
public ScheduledExecutorService create() {
return fakeClock.getScheduledExecutorService();
}
@Override
public void close(ScheduledExecutorService instance) {
assertSame(fakeClock, instance);
}
};
private final Resource<ExecutorService> fakeExecutorResource =
new Resource<ExecutorService>() {
@Override
public ExecutorService create() {
return fakeExecutor.getScheduledExecutorService();
}
@Override
public void close(ExecutorService instance) {
assertSame(fakeExecutor, instance);
}
};
@Mock
private NameResolver.Listener mockListener;
@Captor
private ArgumentCaptor<List<EquivalentAddressGroup>> resultCaptor;
@Captor
private ArgumentCaptor<Status> statusCaptor;
private DnsNameResolver newResolver(String name, int port) {
DnsNameResolver dnsResolver = new DnsNameResolver(
null,
name,
Attributes.newBuilder().set(NameResolver.Factory.PARAMS_DEFAULT_PORT, port).build(),
fakeTimerServiceResource,
fakeExecutorResource);
dnsResolver.setDelegateResolver(mockResolver);
return dnsResolver;
}
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
DnsNameResolver.enableJndi = true;
}
@After
public void noMorePendingTasks() {
assertEquals(0, fakeClock.numPendingTasks());
assertEquals(0, fakeExecutor.numPendingTasks());
}
@Test
public void invalidDnsName() throws Exception {
testInvalidUri(new URI("dns", null, "/[invalid]", null));
}
@Test
public void validIpv6() throws Exception {
testValidUri(new URI("dns", null, "/[::1]", null), "[::1]", DEFAULT_PORT);
}
@Test
public void validDnsNameWithoutPort() throws Exception {
testValidUri(new URI("dns", null, "/foo.googleapis.com", null),
"foo.googleapis.com", DEFAULT_PORT);
}
@Test
public void validDnsNameWithPort() throws Exception {
testValidUri(new URI("dns", null, "/foo.googleapis.com:456", null),
"foo.googleapis.com:456", 456);
}
@Test
public void resolve() throws Exception {
List<InetAddress> answer1 = createAddressList(2);
List<InetAddress> answer2 = createAddressList(1);
String name = "foo.googleapis.com";
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(answer1).addAnswer(answer2);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer1, 81, resultCaptor.getValue());
assertEquals(0, fakeClock.numPendingTasks());
resolver.refresh();
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener, times(2)).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer2, 81, resultCaptor.getValue());
assertEquals(0, fakeClock.numPendingTasks());
resolver.shutdown();
}
@Test
public void retry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
List<InetAddress> answer = createAddressList(2);
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(error).addAnswer(error).addAnswer(answer);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, mockResolver.invocations.poll());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
// First retry scheduled
assertEquals(1, fakeClock.numPendingTasks());
fakeClock.forwardNanos(TimeUnit.MINUTES.toNanos(1) - 1);
assertEquals(1, fakeClock.numPendingTasks());
// First retry
fakeClock.forwardNanos(1);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener, times(2)).onError(statusCaptor.capture());
assertEquals(name, mockResolver.invocations.poll());
status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
// Second retry scheduled
assertEquals(1, fakeClock.numPendingTasks());
fakeClock.forwardNanos(TimeUnit.MINUTES.toNanos(1) - 1);
assertEquals(1, fakeClock.numPendingTasks());
// Second retry
fakeClock.forwardNanos(1);
assertEquals(0, fakeClock.numPendingTasks());
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer, 81, resultCaptor.getValue());
verifyNoMoreInteractions(mockListener);
}
@Test
public void refreshCancelsScheduledRetry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
List<InetAddress> answer = createAddressList(2);
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(error).addAnswer(answer);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, mockResolver.invocations.poll());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
// First retry scheduled
assertEquals(1, fakeClock.numPendingTasks());
resolver.refresh();
assertEquals(1, fakeExecutor.runDueTasks());
// Refresh cancelled the retry
assertEquals(0, fakeClock.numPendingTasks());
verify(mockListener).onAddresses(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, mockResolver.invocations.poll());
assertAnswerMatches(answer, 81, resultCaptor.getValue());
verifyNoMoreInteractions(mockListener);
}
@Test
public void shutdownCancelsScheduledRetry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
DnsNameResolver resolver = newResolver(name, 81);
mockResolver.addAnswer(error);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, mockResolver.invocations.poll());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
// Retry scheduled
assertEquals(1, fakeClock.numPendingTasks());
// Shutdown cancelled the retry
resolver.shutdown();
assertEquals(0, fakeClock.numPendingTasks());
verifyNoMoreInteractions(mockListener);
}
@Test(timeout = 10000)
public void jdkResolverWorks() throws Exception {
DnsNameResolver.DelegateResolver resolver = new DnsNameResolver.JdkResolver();
ResolutionResults results = resolver.resolve("localhost");
// Just check that *something* came back.
assertThat(results.addresses).isNotEmpty();
assertThat(results.txtRecords).isNotNull();
}
@Test(timeout = 10000)
public void jndiResolverWorks() throws Exception {
Assume.assumeTrue(DnsNameResolver.jndiAvailable());
DnsNameResolver.DelegateResolver resolver = new DnsNameResolver.JndiResolver();
ResolutionResults results = null;
try {
results = resolver.resolve("localhost");
} catch (javax.naming.CommunicationException e) {
Assume.assumeNoException(e);
} catch (javax.naming.NameNotFoundException e) {
Assume.assumeNoException(e);
}
assertThat(results.addresses).isEmpty();
assertThat(results.txtRecords).isNotNull();
}
@Test
public void compositeResolverPrefersJdkAddressJndiTxt() throws Exception {
MockResolver jdkDelegate = new MockResolver();
MockResolver jndiDelegate = new MockResolver();
DelegateResolver resolver = new DnsNameResolver.CompositeResolver(jdkDelegate, jndiDelegate);
List<InetAddress> jdkAnswer = createAddressList(2);
jdkDelegate.addAnswer(jdkAnswer, Arrays.asList("jdktxt"));
List<InetAddress> jdniAnswer = createAddressList(2);
jndiDelegate.addAnswer(jdniAnswer, Arrays.asList("jnditxt"));
ResolutionResults results = resolver.resolve("abc");
assertThat(results.addresses).containsExactlyElementsIn(jdkAnswer).inOrder();
assertThat(results.txtRecords).containsExactly("jnditxt");
}
@Test
public void compositeResolverSkipsAbsentJndi() throws Exception {
MockResolver jdkDelegate = new MockResolver();
MockResolver jndiDelegate = null;
DelegateResolver resolver = new DnsNameResolver.CompositeResolver(jdkDelegate, jndiDelegate);
List<InetAddress> jdkAnswer = createAddressList(2);
jdkDelegate.addAnswer(jdkAnswer);
ResolutionResults results = resolver.resolve("abc");
assertThat(results.addresses).containsExactlyElementsIn(jdkAnswer).inOrder();
assertThat(results.txtRecords).isEmpty();
}
private void testInvalidUri(URI uri) {
try {
provider.newNameResolver(uri, NAME_RESOLVER_PARAMS);
fail("Should have failed");
} catch (IllegalArgumentException e) {
// expected
}
}
private void testValidUri(URI uri, String exportedAuthority, int expectedPort) {
DnsNameResolver resolver = provider.newNameResolver(uri, NAME_RESOLVER_PARAMS);
assertNotNull(resolver);
assertEquals(expectedPort, resolver.getPort());
assertEquals(exportedAuthority, resolver.getServiceAuthority());
}
private byte lastByte = 0;
private List<InetAddress> createAddressList(int n) throws UnknownHostException {
List<InetAddress> list = new ArrayList<InetAddress>(n);
for (int i = 0; i < n; i++) {
list.add(InetAddress.getByAddress(new byte[] {127, 0, 0, ++lastByte}));
}
return list;
}
private static void assertAnswerMatches(
List<InetAddress> addrs, int port, List<EquivalentAddressGroup> results) {
assertEquals(addrs.size(), results.size());
for (int i = 0; i < addrs.size(); i++) {
EquivalentAddressGroup addrGroup = results.get(i);
InetSocketAddress socketAddr =
(InetSocketAddress) Iterables.getOnlyElement(addrGroup.getAddresses());
assertEquals("Addr " + i, port, socketAddr.getPort());
assertEquals("Addr " + i, addrs.get(i), socketAddr.getAddress());
}
}
private static class MockResolver extends DnsNameResolver.DelegateResolver {
private final Queue<Object> answers = new LinkedList<Object>();
private final Queue<String> invocations = new LinkedList<String>();
MockResolver addAnswer(List<InetAddress> addresses) {
return addAnswer(addresses, null);
}
MockResolver addAnswer(List<InetAddress> addresses, List<String> txtRecords) {
answers.add(
new ResolutionResults(
addresses,
MoreObjects.firstNonNull(txtRecords, Collections.<String>emptyList())));
return this;
}
MockResolver addAnswer(UnknownHostException ex) {
answers.add(ex);
return this;
}
@SuppressWarnings("unchecked") // explosions acceptable.
@Override
ResolutionResults resolve(String host) throws Exception {
invocations.add(host);
Object answer = answers.poll();
if (answer instanceof UnknownHostException) {
throw (UnknownHostException) answer;
}
return (ResolutionResults) answer;
}
}
}