core: support default method config in service config (#6987)

References:
service config spec change: grpc/grpc-proto#75
c-core implementation grpc/grpc#22232
diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelServiceConfig.java b/core/src/main/java/io/grpc/internal/ManagedChannelServiceConfig.java
index a5d4acc..fc78d22 100644
--- a/core/src/main/java/io/grpc/internal/ManagedChannelServiceConfig.java
+++ b/core/src/main/java/io/grpc/internal/ManagedChannelServiceConfig.java
@@ -37,6 +37,8 @@
  */
 final class ManagedChannelServiceConfig {
 
+  @Nullable
+  private final MethodInfo defaultMethodConfig;
   private final Map<String, MethodInfo> serviceMethodMap;
   private final Map<String, MethodInfo> serviceMap;
   @Nullable
@@ -47,11 +49,13 @@
   private final Map<String, ?> healthCheckingConfig;
 
   ManagedChannelServiceConfig(
+      @Nullable MethodInfo defaultMethodConfig,
       Map<String, MethodInfo> serviceMethodMap,
       Map<String, MethodInfo> serviceMap,
       @Nullable Throttle retryThrottling,
       @Nullable Object loadBalancingConfig,
       @Nullable Map<String, ?> healthCheckingConfig) {
+    this.defaultMethodConfig = defaultMethodConfig;
     this.serviceMethodMap = Collections.unmodifiableMap(new HashMap<>(serviceMethodMap));
     this.serviceMap = Collections.unmodifiableMap(new HashMap<>(serviceMap));
     this.retryThrottling = retryThrottling;
@@ -66,6 +70,7 @@
   static ManagedChannelServiceConfig empty() {
     return
         new ManagedChannelServiceConfig(
+            null,
             new HashMap<String, MethodInfo>(),
             new HashMap<String, MethodInfo>(),
             /* retryThrottling= */ null,
@@ -100,6 +105,7 @@
       // this is surprising, but possible.
       return
           new ManagedChannelServiceConfig(
+              null,
               serviceMethodMap,
               serviceMap,
               retryThrottling,
@@ -107,6 +113,7 @@
               healthCheckingConfig);
     }
 
+    MethodInfo defaultMethodConfig = null;
     for (Map<String, ?> methodConfig : methodConfigs) {
       MethodInfo info = new MethodInfo(
           methodConfig, retryEnabled, maxRetryAttemptsLimit, maxHedgedAttemptsLimit);
@@ -114,13 +121,21 @@
       List<Map<String, ?>> nameList =
           ServiceConfigUtil.getNameListFromMethodConfig(methodConfig);
 
-      checkArgument(
-          nameList != null && !nameList.isEmpty(), "no names in method config %s", methodConfig);
+      if (nameList == null || nameList.isEmpty()) {
+        continue;
+      }
       for (Map<String, ?> name : nameList) {
         String serviceName = ServiceConfigUtil.getServiceFromName(name);
-        checkArgument(!Strings.isNullOrEmpty(serviceName), "missing service name");
         String methodName = ServiceConfigUtil.getMethodFromName(name);
-        if (Strings.isNullOrEmpty(methodName)) {
+        if (Strings.isNullOrEmpty(serviceName)) {
+          checkArgument(
+              Strings.isNullOrEmpty(methodName), "missing service name for method %s", methodName);
+          checkArgument(
+              defaultMethodConfig == null,
+              "Duplicate default method config in service config %s",
+              serviceConfig);
+          defaultMethodConfig = info;
+        } else if (Strings.isNullOrEmpty(methodName)) {
           // Service scoped config
           checkArgument(
               !serviceMap.containsKey(serviceName), "Duplicate service %s", serviceName);
@@ -139,6 +154,7 @@
 
     return
         new ManagedChannelServiceConfig(
+            defaultMethodConfig,
             serviceMethodMap,
             serviceMap,
             retryThrottling,
@@ -165,6 +181,11 @@
     return serviceMethodMap;
   }
 
+  @Nullable
+  MethodInfo getDefaultMethodConfig() {
+    return defaultMethodConfig;
+  }
+
   @VisibleForTesting
   @Nullable
   Object getLoadBalancingConfig() {
diff --git a/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java b/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java
index f27f9ef..9fb91db 100644
--- a/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java
+++ b/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java
@@ -174,14 +174,18 @@
   @CheckForNull
   private MethodInfo getMethodInfo(MethodDescriptor<?, ?> method) {
     ManagedChannelServiceConfig mcsc = managedChannelServiceConfig.get();
-    MethodInfo info = null;
-    if (mcsc != null) {
-      info = mcsc.getServiceMethodMap().get(method.getFullMethodName());
+    if (mcsc == null) {
+      return null;
     }
-    if (info == null && mcsc != null) {
+    MethodInfo info;
+    info = mcsc.getServiceMethodMap().get(method.getFullMethodName());
+    if (info == null) {
       String serviceName = method.getServiceName();
       info = mcsc.getServiceMap().get(serviceName);
     }
+    if (info == null) {
+      info = mcsc.getDefaultMethodConfig();
+    }
     return info;
   }
 
diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java
index ad6c73d..5a7bad5 100644
--- a/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java
+++ b/core/src/test/java/io/grpc/internal/ManagedChannelServiceConfigTest.java
@@ -18,15 +18,22 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import java.util.Collections;
 import java.util.Map;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
 @RunWith(JUnit4.class)
 public class ManagedChannelServiceConfigTest {
 
+  @Rule
+  public final ExpectedException thrown = ExpectedException.none();
+
   @Test
   public void managedChannelServiceConfig_shouldParseHealthCheckingConfig() throws Exception {
     Map<String, ?> rawServiceConfig =
@@ -52,6 +59,83 @@
     assertThat(mcsc.getHealthCheckingConfig()).isNull();
   }
 
+  @Test
+  public void createManagedChannelServiceConfig_failsOnDuplicateMethod() {
+    Map<String, ?> name1 = ImmutableMap.of("service", "service", "method", "method");
+    Map<String, ?> name2 = ImmutableMap.of("service", "service", "method", "method");
+    Map<String, ?> methodConfig = ImmutableMap.of("name", ImmutableList.of(name1, name2));
+    Map<String, ?> serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Duplicate method");
+
+    ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null);
+  }
+
+  @Test
+  public void createManagedChannelServiceConfig_failsOnDuplicateService() {
+    Map<String, ?> name1 = ImmutableMap.of("service", "service");
+    Map<String, ?> name2 = ImmutableMap.of("service", "service");
+    Map<String, ?> methodConfig = ImmutableMap.of("name", ImmutableList.of(name1, name2));
+    Map<String, ?> serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Duplicate service");
+
+    ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null);
+  }
+
+  @Test
+  public void createManagedChannelServiceConfig_failsOnDuplicateServiceMultipleConfig() {
+    Map<String, ?> name1 = ImmutableMap.of("service", "service");
+    Map<String, ?> name2 = ImmutableMap.of("service", "service");
+    Map<String, ?> methodConfig1 = ImmutableMap.of("name", ImmutableList.of(name1));
+    Map<String, ?> methodConfig2 = ImmutableMap.of("name", ImmutableList.of(name2));
+    Map<String, ?> serviceConfig =
+        ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig1, methodConfig2));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("Duplicate service");
+
+    ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null);
+  }
+
+  @Test
+  public void createManagedChannelServiceConfig_failsOnMethodNameWithEmptyServiceName() {
+    Map<String, ?> name = ImmutableMap.of("service", "", "method", "method1");
+    Map<String, ?> methodConfig = ImmutableMap.of("name", ImmutableList.of(name));
+    Map<String, ?> serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("missing service name for method method1");
+
+    ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null);
+  }
+
+  @Test
+  public void createManagedChannelServiceConfig_failsOnMethodNameWithoutServiceName() {
+    Map<String, ?> name = ImmutableMap.of("method", "method1");
+    Map<String, ?> methodConfig = ImmutableMap.of("name", ImmutableList.of(name));
+    Map<String, ?> serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("missing service name for method method1");
+
+    ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null);
+  }
+
+  @Test
+  public void createManagedChannelServiceConfig_failsOnMissingServiceName() {
+    Map<String, ?> name = ImmutableMap.of("method", "method");
+    Map<String, ?> methodConfig = ImmutableMap.of("name", ImmutableList.of(name));
+    Map<String, ?> serviceConfig = ImmutableMap.of("methodConfig", ImmutableList.of(methodConfig));
+
+    thrown.expect(IllegalArgumentException.class);
+    thrown.expectMessage("missing service");
+
+    ManagedChannelServiceConfig.fromServiceConfig(serviceConfig, true, 3, 4, null);
+  }
+
   @SuppressWarnings("unchecked")
   private static Map<String, Object> parseConfig(String json) throws Exception {
     return (Map<String, Object>) JsonParser.parse(json);
diff --git a/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java b/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java
index d339e44..eaa6748 100644
--- a/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java
+++ b/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java
@@ -304,81 +304,61 @@
   }
 
   @Test
-  public void handleUpdate_failsOnMissingServiceName() {
-    JsonObj name = new JsonObj("method", "method");
-    JsonObj methodConfig = new JsonObj("name", new JsonList(name));
-    JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
-
-    thrown.expect(IllegalArgumentException.class);
-    thrown.expectMessage("missing service");
-
-    ManagedChannelServiceConfig parsedServiceConfig =
-        createManagedChannelServiceConfig(serviceConfig);
-
-    interceptor.handleUpdate(parsedServiceConfig);
-  }
-
-  @Test
-  public void handleUpdate_failsOnDuplicateMethod() {
-    JsonObj name1 = new JsonObj("service", "service", "method", "method");
-    JsonObj name2 = new JsonObj("service", "service", "method", "method");
-    JsonObj methodConfig = new JsonObj("name", new JsonList(name1, name2));
-    JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
-
-    thrown.expect(IllegalArgumentException.class);
-    thrown.expectMessage("Duplicate method");
-
-    ManagedChannelServiceConfig parsedServiceConfig =
-        createManagedChannelServiceConfig(serviceConfig);
-
-    interceptor.handleUpdate(parsedServiceConfig);
-  }
-
-  @Test
-  public void handleUpdate_failsOnEmptyName() {
+  public void handleUpdate_onEmptyName() {
     JsonObj methodConfig = new JsonObj();
     JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
-
-    thrown.expect(IllegalArgumentException.class);
-    thrown.expectMessage("no names in method config");
-
     ManagedChannelServiceConfig parsedServiceConfig =
         createManagedChannelServiceConfig(serviceConfig);
 
     interceptor.handleUpdate(parsedServiceConfig);
+
+    assertThat(interceptor.managedChannelServiceConfig.get().getDefaultMethodConfig()).isNull();
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMap()).isEmpty();
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMethodMap()).isEmpty();
   }
 
   @Test
-  public void handleUpdate_failsOnDuplicateService() {
-    JsonObj name1 = new JsonObj("service", "service");
-    JsonObj name2 = new JsonObj("service", "service");
-    JsonObj methodConfig = new JsonObj("name", new JsonList(name1, name2));
+  public void handleUpdate_onDefaultMethodConfig() {
+    JsonObj name = new JsonObj();
+    JsonObj methodConfig = new JsonObj("name", new JsonList(name));
     JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
-
-    thrown.expect(IllegalArgumentException.class);
-    thrown.expectMessage("Duplicate service");
-
     ManagedChannelServiceConfig parsedServiceConfig =
         createManagedChannelServiceConfig(serviceConfig);
-
     interceptor.handleUpdate(parsedServiceConfig);
-  }
+    assertThat(interceptor.managedChannelServiceConfig.get().getDefaultMethodConfig())
+        .isEqualTo(new MethodInfo(methodConfig, false, 1, 1));
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMap()).isEmpty();
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMethodMap()).isEmpty();
 
-  @Test
-  public void handleUpdate_failsOnDuplicateServiceMultipleConfig() {
-    JsonObj name1 = new JsonObj("service", "service");
-    JsonObj name2 = new JsonObj("service", "service");
-    JsonObj methodConfig1 = new JsonObj("name", new JsonList(name1));
-    JsonObj methodConfig2 = new JsonObj("name", new JsonList(name2));
-    JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig1, methodConfig2));
-
-    thrown.expect(IllegalArgumentException.class);
-    thrown.expectMessage("Duplicate service");
-
-    ManagedChannelServiceConfig parsedServiceConfig =
-        createManagedChannelServiceConfig(serviceConfig);
-
+    name = new JsonObj("method", "");
+    methodConfig = new JsonObj("name", new JsonList(name));
+    serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
+    parsedServiceConfig = createManagedChannelServiceConfig(serviceConfig);
     interceptor.handleUpdate(parsedServiceConfig);
+    assertThat(interceptor.managedChannelServiceConfig.get().getDefaultMethodConfig())
+        .isEqualTo(new MethodInfo(methodConfig, false, 1, 1));
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMap()).isEmpty();
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMethodMap()).isEmpty();
+
+    name = new JsonObj("service", "");
+    methodConfig = new JsonObj("name", new JsonList(name));
+    serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
+    parsedServiceConfig = createManagedChannelServiceConfig(serviceConfig);
+    interceptor.handleUpdate(parsedServiceConfig);
+    assertThat(interceptor.managedChannelServiceConfig.get().getDefaultMethodConfig())
+        .isEqualTo(new MethodInfo(methodConfig, false, 1, 1));
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMap()).isEmpty();
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMethodMap()).isEmpty();
+
+    name = new JsonObj("service", "", "method", "");
+    methodConfig = new JsonObj("name", new JsonList(name));
+    serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
+    parsedServiceConfig = createManagedChannelServiceConfig(serviceConfig);
+    interceptor.handleUpdate(parsedServiceConfig);
+    assertThat(interceptor.managedChannelServiceConfig.get().getDefaultMethodConfig())
+        .isEqualTo(new MethodInfo(methodConfig, false, 1, 1));
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMap()).isEmpty();
+    assertThat(interceptor.managedChannelServiceConfig.get().getServiceMethodMap()).isEmpty();
   }
 
   @Test
@@ -426,6 +406,61 @@
   }
 
   @Test
+  public void interceptCall_matchNames() {
+    JsonObj name0 = new JsonObj();
+    JsonObj name1 = new JsonObj("service", "service");
+    JsonObj name2 = new JsonObj("service", "service", "method", "method");
+    JsonObj methodConfig0 = new JsonObj(
+        "name", new JsonList(name0), "maxRequestMessageBytes", 5d);
+    JsonObj methodConfig1 = new JsonObj(
+        "name", new JsonList(name1), "maxRequestMessageBytes", 6d);
+    JsonObj methodConfig2 = new JsonObj(
+        "name", new JsonList(name2), "maxRequestMessageBytes", 7d);
+    JsonObj serviceConfig =
+        new JsonObj("methodConfig", new JsonList(methodConfig0, methodConfig1, methodConfig2));
+    ManagedChannelServiceConfig parsedServiceConfig =
+        createManagedChannelServiceConfig(serviceConfig);
+
+    interceptor.handleUpdate(parsedServiceConfig);
+
+    String fullMethodName =
+        MethodDescriptor.generateFullMethodName("service", "method");
+    MethodDescriptor<Void, Void> methodDescriptor =
+        MethodDescriptor.newBuilder(new NoopMarshaller(), new NoopMarshaller())
+            .setType(MethodType.UNARY)
+            .setFullMethodName(fullMethodName)
+            .build();
+    interceptor.interceptCall(
+        methodDescriptor, CallOptions.DEFAULT, channel);
+    verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
+    assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(7);
+
+    fullMethodName =
+        MethodDescriptor.generateFullMethodName("service", "method2");
+    methodDescriptor =
+        MethodDescriptor.newBuilder(new NoopMarshaller(), new NoopMarshaller())
+            .setType(MethodType.UNARY)
+            .setFullMethodName(fullMethodName)
+            .build();
+    interceptor.interceptCall(
+        methodDescriptor, CallOptions.DEFAULT, channel);
+    verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
+    assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(6);
+
+    fullMethodName =
+        MethodDescriptor.generateFullMethodName("service2", "method");
+    methodDescriptor =
+        MethodDescriptor.newBuilder(new NoopMarshaller(), new NoopMarshaller())
+            .setType(MethodType.UNARY)
+            .setFullMethodName(fullMethodName)
+            .build();
+    interceptor.interceptCall(
+        methodDescriptor, CallOptions.DEFAULT, channel);
+    verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
+    assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(5);
+  }
+
+  @Test
   public void methodInfo_validateDeadline() {
     JsonObj name = new JsonObj("service", "service");
     JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "10000000000000000s");
diff --git a/core/src/test/java/io/grpc/internal/ServiceConfigStateTest.java b/core/src/test/java/io/grpc/internal/ServiceConfigStateTest.java
index 868fb15..4390ff4 100644
--- a/core/src/test/java/io/grpc/internal/ServiceConfigStateTest.java
+++ b/core/src/test/java/io/grpc/internal/ServiceConfigStateTest.java
@@ -39,12 +39,14 @@
 public class ServiceConfigStateTest {
 
   private final ManagedChannelServiceConfig serviceConfig1 = new ManagedChannelServiceConfig(
+      null,
       Collections.<String, MethodInfo>emptyMap(),
       Collections.<String, MethodInfo>emptyMap(),
       null,
       null,
       null);
   private final ManagedChannelServiceConfig serviceConfig2 = new ManagedChannelServiceConfig(
+      null,
       Collections.<String, MethodInfo>emptyMap(),
       Collections.<String, MethodInfo>emptyMap(),
       null,
@@ -428,6 +430,7 @@
   public void lookup_default_onPresent_onPresent() {
     ServiceConfigState scs = new ServiceConfigState(serviceConfig1, true);
     ManagedChannelServiceConfig serviceConfig3 = new ManagedChannelServiceConfig(
+        null,
         Collections.<String, MethodInfo>emptyMap(),
         Collections.<String, MethodInfo>emptyMap(),
         null,