Repository: grpc/grpc-java
Issue: OutOfBounds exception with NameResolvers and TransportSet

Long story short, it appears there's an explicit array.get(0) that happens on the first outbound call, but if the NameResolver hasn't found any servers yet, this causes an exception.  Stack trace can be found below(SpectatorInterceptor is a proprietary metrics component, and the EurekaNameResolver simply uses Eureka to resolve a set of IP + port that can then be used to load balance).  In the case below, the list of servers the EurekaNameResolver passes to onUpdate is empty.

```
SEVERE: Exception while executing runnable io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$2@7d8bd17e
java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
    at java.util.ArrayList.rangeCheck(ArrayList.java:653)
    at java.util.ArrayList.get(ArrayList.java:429)
    at java.util.Collections$UnmodifiableList.get(Collections.java:1309)
    at io.grpc.internal.TransportSet.scheduleConnection(TransportSet.java:197)
    at io.grpc.internal.TransportSet.obtainActiveTransport(TransportSet.java:169)
    at io.grpc.internal.ManagedChannelImpl$3.getTransport(ManagedChannelImpl.java:381)
    at io.grpc.SimpleLoadBalancerFactory$SimpleLoadBalancer$1.get(SimpleLoadBalancerFactory.java:121)
    at io.grpc.SimpleLoadBalancerFactory$SimpleLoadBalancer$1.get(SimpleLoadBalancerFactory.java:119)
    at io.grpc.internal.BlankFutureProvider$FulfillmentBatch.link(BlankFutureProvider.java:128)
    at io.grpc.SimpleLoadBalancerFactory$SimpleLoadBalancer.handleResolvedAddresses(SimpleLoadBalancerFactory.java:119)
    at io.grpc.internal.ManagedChannelImpl$2.onUpdate(ManagedChannelImpl.java:165)
    at com.netflix.grpc.nameresolver.eureka.EurekaNameResolver$1.onNext(EurekaNameResolver.java:64)
    at com.netflix.grpc.nameresolver.eureka.EurekaNameResolver$1.onNext(EurekaNameResolver.java:61)
    at io.grpc.stub.ClientCalls$StreamObserverToCallListenerAdapter.onMessage(ClientCalls.java:305)
    at io.grpc.ForwardingClientCallListener.onMessage(ForwardingClientCallListener.java:51)
    at io.grpc.ForwardingClientCallListener.onMessage(ForwardingClientCallListener.java:51)
    at com.netflix.grpc.interceptor.spectator.SpectatorMetricsClientInterceptor$1$1.onMessage(SpectatorMetricsClientInterceptor.java:44)
    at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$2.runInContext(ClientCallImpl.java:423)
    at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:54)
    at io.grpc.internal.SerializingExecutor$TaskRunner.run(SerializingExecutor.java:154)
    at java.util.concurrent.ThreadPoolExecutorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
```


Changed files:
core/src/main/java/io/grpc/NameResolver.java, core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java, core/src/main/java/io/grpc/EquivalentAddressGroup.java, core/src/main/java/io/grpc/internal/ManagedChannelImpl.java, core/src/main/java/io/grpc/LoadBalancer.java

diff --git a/core/src/main/java/io/grpc/EquivalentAddressGroup.java b/core/src/main/java/io/grpc/EquivalentAddressGroup.java
index ef64a90be..49efbbf6a 100644
--- a/core/src/main/java/io/grpc/EquivalentAddressGroup.java
+++ b/core/src/main/java/io/grpc/EquivalentAddressGroup.java
@@ -31,6 +31,8 @@
 
 package io.grpc;
 
+import com.google.common.base.Preconditions;
+
 import java.net.SocketAddress;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -49,6 +51,7 @@ public final class EquivalentAddressGroup {
   private final List<SocketAddress> addrs;
 
   public EquivalentAddressGroup(List<SocketAddress> addrs) {
+    Preconditions.checkArgument(!addrs.isEmpty(), "addrs is empty");
     this.addrs = Collections.unmodifiableList(new ArrayList<SocketAddress>(addrs));
   }
 
diff --git a/core/src/main/java/io/grpc/LoadBalancer.java b/core/src/main/java/io/grpc/LoadBalancer.java
index fbc2f7991..fec3ae222 100644
--- a/core/src/main/java/io/grpc/LoadBalancer.java
+++ b/core/src/main/java/io/grpc/LoadBalancer.java
@@ -68,6 +68,9 @@ public abstract class LoadBalancer<T> {
    * Handles newly resolved addresses and service config from name resolution system.
    *
    * <p>Implementations should not modify the given {@code servers}.
+   *
+   * @param servers the resolved server addresses. Never empty.
+   * @param config extra configuration data from naming system.
    */
   public void handleResolvedAddresses(List<ResolvedServerInfo> servers, Attributes config) { }
 
diff --git a/core/src/main/java/io/grpc/NameResolver.java b/core/src/main/java/io/grpc/NameResolver.java
index b6c8e4d12..3e5210743 100644
--- a/core/src/main/java/io/grpc/NameResolver.java
+++ b/core/src/main/java/io/grpc/NameResolver.java
@@ -119,6 +119,9 @@ public abstract class NameResolver {
      * Handles updates on resolved addresses and config.
      *
      * <p>Implementations will not modify the given {@code servers}.
+     *
+     * @param servers the resolved server addresses. An empty list will trigger {@link #onError}
+     * @param config extra configuration data from naming system
      */
     void onUpdate(List<ResolvedServerInfo> servers, Attributes config);
 
diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java
index 82f903a85..284cba4b1 100644
--- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java
+++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java
@@ -168,7 +168,11 @@ public final class ManagedChannelImpl extends ManagedChannel implements WithLogI
     this.nameResolver.start(new NameResolver.Listener() {
       @Override
       public void onUpdate(List<ResolvedServerInfo> servers, Attributes config) {
-        loadBalancer.handleResolvedAddresses(servers, config);
+        if (servers.isEmpty()) {
+          onError(Status.UNAVAILABLE.withDescription("NameResolver returned an empty list"));
+        } else {
+          loadBalancer.handleResolvedAddresses(servers, config);
+        }
       }
 
       @Override
diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java
index ed49239d7..ad75482e1 100644
--- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java
+++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java
@@ -362,6 +362,27 @@ public class ManagedChannelImplTest {
     Status status = statusCaptor.getValue();
     assertSame(error.getCode(), status.getCode());
     assertSame(error.getCause(), status.getCause());
+    assertEquals(1, loadBalancerFactory.balancers.size());
+    verify(loadBalancerFactory.balancers.get(0)).handleNameResolutionError(same(error));
+  }
+
+  @Test
+  public void nameResolverReturnsEmptyList() {
+    String errorDescription = "NameResolver returned an empty list";
+    ManagedChannel channel = createChannel(
+        new FakeNameResolverFactory(new ArrayList<ResolvedServerInfo>()), NO_INTERCEPTOR);
+    ClientCall<String, Integer> call = channel.newCall(method, CallOptions.DEFAULT);
+    call.start(mockCallListener, new Metadata());
+    ArgumentCaptor<Status> statusCaptor = ArgumentCaptor.forClass(Status.class);
+    verify(mockCallListener, timeout(1000)).onClose(statusCaptor.capture(), any(Metadata.class));
+    Status status = statusCaptor.getValue();
+    assertSame(Status.Code.UNAVAILABLE, status.getCode());
+    assertTrue(status.getDescription(), status.getDescription().contains(errorDescription));
+    assertEquals(1, loadBalancerFactory.balancers.size());
+    verify(loadBalancerFactory.balancers.get(0)).handleNameResolutionError(statusCaptor.capture());
+    status = statusCaptor.getValue();
+    assertSame(Status.Code.UNAVAILABLE, status.getCode());
+    assertEquals(errorDescription, status.getDescription());
   }
 
   @Test