blob: ff5bb94925fd2691525ad95167993bc140fd8ca1 [file] [log] [blame]
/*
* Copyright 2015, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc.internal;
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.collect.Iterables;
import io.grpc.Attributes;
import io.grpc.NameResolver;
import io.grpc.ResolvedServerInfoGroup;
import io.grpc.Status;
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.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.junit.After;
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 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<ResolvedServerInfoGroup>> resultCaptor;
@Captor
private ArgumentCaptor<Status> statusCaptor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@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 {
InetAddress[] answer1 = createAddressList(2);
InetAddress[] answer2 = createAddressList(1);
String name = "foo.googleapis.com";
MockResolver resolver = new MockResolver(name, 81, answer1, answer2);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onUpdate(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertAnswerMatches(answer1, 81, Iterables.getOnlyElement(resultCaptor.getValue()));
assertEquals(0, fakeClock.numPendingTasks());
resolver.refresh();
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener, times(2)).onUpdate(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertAnswerMatches(answer2, 81, Iterables.getOnlyElement(resultCaptor.getValue()));
assertEquals(0, fakeClock.numPendingTasks());
resolver.shutdown();
}
@Test
public void retry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
InetAddress[] answer = createAddressList(2);
MockResolver resolver = new MockResolver(name, 81, error, error, answer);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, resolver.invocations.poll());
Status status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
// First retry scheduled
assertEquals(1, fakeClock.numPendingTasks());
fakeClock.forwardMillis(TimeUnit.MINUTES.toMillis(1) - 1);
assertEquals(1, fakeClock.numPendingTasks());
// First retry
fakeClock.forwardMillis(1);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener, times(2)).onError(statusCaptor.capture());
assertEquals(name, resolver.invocations.poll());
status = statusCaptor.getValue();
assertEquals(Status.Code.UNAVAILABLE, status.getCode());
assertSame(error, status.getCause());
// Second retry scheduled
assertEquals(1, fakeClock.numPendingTasks());
fakeClock.forwardMillis(TimeUnit.MINUTES.toMillis(1) - 1);
assertEquals(1, fakeClock.numPendingTasks());
// Second retry
fakeClock.forwardMillis(1);
assertEquals(0, fakeClock.numPendingTasks());
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onUpdate(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertAnswerMatches(answer, 81, Iterables.getOnlyElement(resultCaptor.getValue()));
verifyNoMoreInteractions(mockListener);
}
@Test
public void refreshCancelsScheduledRetry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
InetAddress[] answer = createAddressList(2);
MockResolver resolver = new MockResolver(name, 81, error, answer);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, resolver.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).onUpdate(resultCaptor.capture(), any(Attributes.class));
assertEquals(name, resolver.invocations.poll());
assertAnswerMatches(answer, 81, Iterables.getOnlyElement(resultCaptor.getValue()));
verifyNoMoreInteractions(mockListener);
}
@Test
public void shutdownCancelsScheduledRetry() throws Exception {
String name = "foo.googleapis.com";
UnknownHostException error = new UnknownHostException(name);
MockResolver resolver = new MockResolver(name, 81, error);
resolver.start(mockListener);
assertEquals(1, fakeExecutor.runDueTasks());
verify(mockListener).onError(statusCaptor.capture());
assertEquals(name, resolver.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);
}
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 InetAddress[] createAddressList(int n) throws UnknownHostException {
InetAddress[] list = new InetAddress[n];
for (int i = 0; i < n; i++) {
list[i] = InetAddress.getByAddress(new byte[] {127, 0, 0, ++lastByte});
}
return list;
}
private static void assertAnswerMatches(InetAddress[] addrs, int port,
ResolvedServerInfoGroup result) {
assertEquals(addrs.length, result.getResolvedServerInfoList().size());
for (int i = 0; i < addrs.length; i++) {
InetSocketAddress socketAddr = (InetSocketAddress) result.getResolvedServerInfoList().get(
i).getAddress();
assertEquals("Addr " + i, port, socketAddr.getPort());
assertEquals("Addr " + i, addrs[i], socketAddr.getAddress());
}
}
private class MockResolver extends DnsNameResolver {
final LinkedList<Object> answers = new LinkedList<Object>();
final LinkedList<String> invocations = new LinkedList<String>();
MockResolver(String name, int defaultPort, Object ... answers) {
super(null, name, Attributes.newBuilder().set(
NameResolver.Factory.PARAMS_DEFAULT_PORT, defaultPort).build(), fakeTimerServiceResource,
fakeExecutorResource);
for (Object answer : answers) {
this.answers.add(answer);
}
}
@Override
InetAddress[] getAllByName(String host) throws UnknownHostException {
invocations.add(host);
Object answer = answers.poll();
if (answer instanceof UnknownHostException) {
throw (UnknownHostException) answer;
}
return (InetAddress[]) answer;
}
}
}