diff --git a/core/src/main/java/io/grpc/ForwardingChannelBuilder.java b/core/src/main/java/io/grpc/ForwardingChannelBuilder.java index 8b9cb6ef02a..7f7c1e66df9 100644 --- a/core/src/main/java/io/grpc/ForwardingChannelBuilder.java +++ b/core/src/main/java/io/grpc/ForwardingChannelBuilder.java @@ -18,8 +18,10 @@ import com.google.common.base.MoreObjects; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; /** * A {@link ManagedChannelBuilder} that delegates all its builder method to another builder by @@ -242,6 +244,18 @@ public T proxyDetector(ProxyDetector proxyDetector) { return thisT(); } + @Override + public T defaultServiceConfig(@Nullable Map serviceConfig) { + delegate().defaultServiceConfig(serviceConfig); + return thisT(); + } + + @Override + public T lookUpServiceConfig(boolean enable) { + delegate().lookUpServiceConfig(enable); + return thisT(); + } + /** * Returns the {@link ManagedChannel} built by the delegate by default. Overriding method can * return different value. diff --git a/core/src/main/java/io/grpc/ManagedChannelBuilder.java b/core/src/main/java/io/grpc/ManagedChannelBuilder.java index 677701d8819..e7e7d9e44fb 100644 --- a/core/src/main/java/io/grpc/ManagedChannelBuilder.java +++ b/core/src/main/java/io/grpc/ManagedChannelBuilder.java @@ -18,8 +18,10 @@ import com.google.common.base.Preconditions; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import javax.annotation.Nullable; /** * A builder for {@link ManagedChannel} instances. @@ -537,6 +539,60 @@ public T proxyDetector(ProxyDetector proxyDetector) { throw new UnsupportedOperationException(); } + /** + * Provides a service config to the channel. The channel will use the default service config when + * the name resolver provides no service config or if the channel disables lookup service config + * from name resolver (see {@link #lookUpServiceConfig(boolean)}). The argument + * {@code serviceConfig} is a nested map representing a Json object in the most natural way: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Json entryJava Type
object{@link Map}
array{@link List}
string{@link String}
number{@link Double}
boolean{@link Boolean}
null{@code null}
+ * + *

If null is passed, then there will be no default service config. + * + * @throws IllegalArgumentException When the given serviceConfig is invalid or the current version + * of grpc library can not parse it gracefully. The state of the builder is unchanged if + * an exception is thrown. + * @return this + * @since 1.20.0 + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/5189") + public T defaultServiceConfig(@Nullable Map serviceConfig) { + throw new UnsupportedOperationException(); + } + + /** + * Enables or disables service config look-up from the naming system. Enabled by default. + * + * @return this + * @since 1.20.0 + */ + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/5189") + public T lookUpServiceConfig(boolean enable) { + throw new UnsupportedOperationException(); + } + /** * Builds a channel using the given parameters. * diff --git a/core/src/main/java/io/grpc/internal/AbstractManagedChannelImplBuilder.java b/core/src/main/java/io/grpc/internal/AbstractManagedChannelImplBuilder.java index b72a840b843..1b1a81cffc1 100644 --- a/core/src/main/java/io/grpc/internal/AbstractManagedChannelImplBuilder.java +++ b/core/src/main/java/io/grpc/internal/AbstractManagedChannelImplBuilder.java @@ -41,7 +41,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; @@ -139,6 +141,10 @@ public static ManagedChannelBuilder forTarget(String target) { InternalChannelz channelz = InternalChannelz.instance(); int maxTraceEvents; + @Nullable + Map defaultServiceConfig; + boolean lookUpServiceConfig = true; + protected TransportTracer.Factory transportTracerFactory = TransportTracer.getDefaultFactory(); private int maxInboundMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; @@ -379,6 +385,78 @@ public T proxyDetector(@Nullable ProxyDetector proxyDetector) { return thisT(); } + @Override + public T defaultServiceConfig(@Nullable Map serviceConfig) { + // TODO(notcarl): use real parsing + defaultServiceConfig = checkMapEntryTypes(serviceConfig); + return thisT(); + } + + @Nullable + private static Map checkMapEntryTypes(@Nullable Map map) { + if (map == null) { + return null; + } + // Not using ImmutableMap.Builder because of extra guava dependency for Android. + Map parsedMap = new LinkedHashMap<>(); + for (Map.Entry entry : map.entrySet()) { + checkArgument( + entry.getKey() instanceof String, + "The key of the entry '%s' is not of String type", entry); + + String key = (String) entry.getKey(); + Object value = entry.getValue(); + if (value == null) { + parsedMap.put(key, null); + } else if (value instanceof Map) { + parsedMap.put(key, checkMapEntryTypes((Map) value)); + } else if (value instanceof List) { + parsedMap.put(key, checkListEntryTypes((List) value)); + } else if (value instanceof String) { + parsedMap.put(key, value); + } else if (value instanceof Double) { + parsedMap.put(key, value); + } else if (value instanceof Boolean) { + parsedMap.put(key, value); + } else { + throw new IllegalArgumentException( + "The value of the map entry '" + entry + "' is of type '" + value.getClass() + + "', which is not supported"); + } + } + return Collections.unmodifiableMap(parsedMap); + } + + private static List checkListEntryTypes(List list) { + List parsedList = new ArrayList<>(list.size()); + for (Object value : list) { + if (value == null) { + parsedList.add(null); + } else if (value instanceof Map) { + parsedList.add(checkMapEntryTypes((Map) value)); + } else if (value instanceof List) { + parsedList.add(checkListEntryTypes((List) value)); + } else if (value instanceof String) { + parsedList.add(value); + } else if (value instanceof Double) { + parsedList.add(value); + } else if (value instanceof Boolean) { + parsedList.add(value); + } else { + throw new IllegalArgumentException( + "The entry '" + value + "' is of type '" + value.getClass() + + "', which is not supported"); + } + } + return Collections.unmodifiableList(parsedList); + } + + @Override + public T lookUpServiceConfig(boolean enable) { + this.lookUpServiceConfig = enable; + return thisT(); + } + /** * Disable or enable stats features. Enabled by default. * @@ -490,7 +568,7 @@ final List getEffectiveInterceptors() { /** * Subclasses can override this method to provide a default port to {@link NameResolver} for use * in cases where the target string doesn't include a port. The default implementation returns - * {@link GrpcUtil.DEFAULT_PORT_SSL}. + * {@link GrpcUtil#DEFAULT_PORT_SSL}. */ protected int getDefaultPort() { return GrpcUtil.DEFAULT_PORT_SSL; diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 01fdbd4fa9d..ea1f21acd64 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -237,9 +237,17 @@ public void uncaughtException(Thread t, Throwable e) { // Must be mutated and read from syncContext @CheckForNull private Boolean haveBackends; // a flag for doing channel tracing when flipped - // Must be mutated and read from syncContext + // Must be mutated and read from constructor or syncContext + // TODO(notcarl): check this value when error in service config resolution @Nullable private Map lastServiceConfig; // used for channel tracing when value changed + @Nullable + private final Map defaultServiceConfig; + // Must be mutated and read from constructor or syncContext + // See service config error handling spec for reference. + // TODO(notcarl): check this value when error in service config resolution + private boolean waitingForServiceConfig = true; + private final boolean lookUpServiceConfig; // One instance per channel. private final ChannelBufferMeter channelBufferUsed = new ChannelBufferMeter(); @@ -581,6 +589,9 @@ ClientStream newSubstream(ClientStreamTracer.Factory tracerFactory, Metadata new serviceConfigInterceptor = new ServiceConfigInterceptor( retryEnabled, builder.maxRetryAttempts, builder.maxHedgedAttempts); + this.defaultServiceConfig = builder.defaultServiceConfig; + this.lastServiceConfig = defaultServiceConfig; + this.lookUpServiceConfig = builder.lookUpServiceConfig; Channel channel = new RealChannel(nameResolver.getServiceAuthority()); channel = ClientInterceptors.intercept(channel, serviceConfigInterceptor); if (builder.binlog != null) { @@ -621,6 +632,23 @@ public CallTracer create() { channelCallTracer = callTracerFactory.create(); this.channelz = checkNotNull(builder.channelz); channelz.addRootChannel(this); + + if (!lookUpServiceConfig) { + if (defaultServiceConfig != null) { + channelLogger.log( + ChannelLogLevel.INFO, "Service config look-up disabled, using default service config"); + } + handleServiceConfigUpdate(); + } + } + + // May only be called in constructor or syncContext + private void handleServiceConfigUpdate() { + waitingForServiceConfig = false; + serviceConfigInterceptor.handleUpdate(lastServiceConfig); + if (retryEnabled) { + throttle = ServiceConfigUtil.getThrottlePolicy(lastServiceConfig); + } } @VisibleForTesting @@ -1278,32 +1306,57 @@ private class NameResolverListenerImpl implements NameResolver.Listener { } @Override - public void onAddresses(final List servers, final Attributes config) { + public void onAddresses(final List servers, final Attributes attrs) { final class NamesResolved implements Runnable { + + @SuppressWarnings("ReferenceEquality") @Override public void run() { channelLogger.log( - ChannelLogLevel.DEBUG, "Resolved address: {0}, config={1}", servers, config); + ChannelLogLevel.DEBUG, "Resolved address: {0}, config={1}", servers, attrs); if (haveBackends == null || !haveBackends) { channelLogger.log(ChannelLogLevel.INFO, "Address resolved: {0}", servers); haveBackends = true; } - final Map serviceConfig = - config.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG); - if (serviceConfig != null && !serviceConfig.equals(lastServiceConfig)) { - channelLogger.log(ChannelLogLevel.INFO, "Service config changed"); - lastServiceConfig = serviceConfig; - } nameResolverBackoffPolicy = null; - if (serviceConfig != null) { - try { - serviceConfigInterceptor.handleUpdate(serviceConfig); - if (retryEnabled) { - throttle = ServiceConfigUtil.getThrottlePolicy(serviceConfig); + // Assuming no error in config resolution for now. + final Map serviceConfig = + attrs.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG); + Map effectiveServiceConfig; + if (!lookUpServiceConfig) { + if (serviceConfig != null) { + channelLogger.log( + ChannelLogLevel.INFO, + "Service config from name resolver discarded by channel settings"); + } + effectiveServiceConfig = defaultServiceConfig; + } else { + // Try to use config if returned from name resolver + // Otherwise, try to use the default config if available + if (serviceConfig != null) { + effectiveServiceConfig = serviceConfig; + } else { + effectiveServiceConfig = defaultServiceConfig; + if (defaultServiceConfig != null) { + channelLogger.log( + ChannelLogLevel.INFO, + "Received no service config, using default service config"); } + } + + // FIXME(notcarl): reference equality is not right (although not harmful) right now. + // Name resolver should return the same config if txt record is the same + if (effectiveServiceConfig != lastServiceConfig) { + channelLogger.log(ChannelLogLevel.INFO, + "Service config changed{0}", effectiveServiceConfig == null ? " to null" : ""); + lastServiceConfig = effectiveServiceConfig; + } + + try { + handleServiceConfigUpdate(); } catch (RuntimeException re) { logger.log( Level.WARNING, @@ -1318,7 +1371,13 @@ public void run() { handleErrorInSyncContext(Status.UNAVAILABLE.withDescription( "Name resolver " + resolver + " returned an empty list")); } else { - helper.lb.handleResolvedAddressGroups(servers, config); + Attributes effectiveAttrs = attrs; + if (effectiveServiceConfig != serviceConfig) { + effectiveAttrs = attrs.toBuilder() + .set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, effectiveServiceConfig) + .build(); + } + helper.lb.handleResolvedAddressGroups(servers, effectiveAttrs); } } } diff --git a/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java b/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java index a7e9f45a84e..96ff37f1905 100644 --- a/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java +++ b/core/src/main/java/io/grpc/internal/ServiceConfigInterceptor.java @@ -26,11 +26,12 @@ import io.grpc.Deadline; import io.grpc.MethodDescriptor; import io.grpc.internal.ManagedChannelServiceConfig.MethodInfo; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Modifies RPCs in conformance with a Service Config. @@ -47,7 +48,7 @@ final class ServiceConfigInterceptor implements ClientInterceptor { private final int maxHedgedAttemptsLimit; // Setting this to true and observing this equal to true are run in different threads. - private volatile boolean nameResolveComplete; + private volatile boolean initComplete; ServiceConfigInterceptor( boolean retryEnabled, int maxRetryAttemptsLimit, int maxHedgedAttemptsLimit) { @@ -56,11 +57,17 @@ final class ServiceConfigInterceptor implements ClientInterceptor { this.maxHedgedAttemptsLimit = maxHedgedAttemptsLimit; } - void handleUpdate(@Nonnull Map serviceConfig) { - ManagedChannelServiceConfig conf = ManagedChannelServiceConfig.fromServiceConfig( - serviceConfig, retryEnabled, maxRetryAttemptsLimit, maxHedgedAttemptsLimit); + void handleUpdate(@Nullable Map serviceConfig) { + ManagedChannelServiceConfig conf; + if (serviceConfig == null) { + conf = new ManagedChannelServiceConfig( + new HashMap(), new HashMap()); + } else { + conf = ManagedChannelServiceConfig.fromServiceConfig( + serviceConfig, retryEnabled, maxRetryAttemptsLimit, maxHedgedAttemptsLimit); + } managedChannelServiceConfig.set(conf); - nameResolveComplete = true; + initComplete = true; } static final CallOptions.Key RETRY_POLICY_KEY = @@ -72,7 +79,7 @@ void handleUpdate(@Nonnull Map serviceConfig) { public ClientCall interceptCall( final MethodDescriptor method, CallOptions callOptions, Channel next) { if (retryEnabled) { - if (nameResolveComplete) { + if (initComplete) { final RetryPolicy retryPolicy = getRetryPolicyFromConfig(method); final class ImmediateRetryPolicyProvider implements RetryPolicy.Provider { @Override @@ -106,7 +113,7 @@ final class DelayedRetryPolicyProvider implements RetryPolicy.Provider { */ @Override public RetryPolicy get() { - if (!nameResolveComplete) { + if (!initComplete) { return RetryPolicy.DEFAULT; } return getRetryPolicyFromConfig(method); @@ -122,7 +129,7 @@ final class DelayedHedgingPolicyProvider implements HedgingPolicy.Provider { */ @Override public HedgingPolicy get() { - if (!nameResolveComplete) { + if (!initComplete) { return HedgingPolicy.DEFAULT; } HedgingPolicy hedgingPolicy = getHedgingPolicyFromConfig(method); diff --git a/core/src/test/java/io/grpc/internal/AbstractManagedChannelImplBuilderTest.java b/core/src/test/java/io/grpc/internal/AbstractManagedChannelImplBuilderTest.java index 45d03bedc50..3310e919e8e 100644 --- a/core/src/test/java/io/grpc/internal/AbstractManagedChannelImplBuilderTest.java +++ b/core/src/test/java/io/grpc/internal/AbstractManagedChannelImplBuilderTest.java @@ -42,7 +42,11 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import org.junit.Rule; @@ -429,6 +433,70 @@ public void disableRetry() { assertFalse(builder.retryEnabled); } + @Test + public void defaultServiceConfig_nullKey() { + Builder builder = new Builder("target"); + Map config = new HashMap<>(); + config.put(null, "val"); + + thrown.expect(IllegalArgumentException.class); + builder.defaultServiceConfig(config); + } + + @Test + public void defaultServiceConfig_intKey() { + Builder builder = new Builder("target"); + Map subConfig = new HashMap<>(); + subConfig.put(3, "val"); + Map config = new HashMap<>(); + config.put("key", subConfig); + + thrown.expect(IllegalArgumentException.class); + builder.defaultServiceConfig(config); + } + + @Test + public void defaultServiceConfig_intValue() { + Builder builder = new Builder("target"); + Map config = new HashMap<>(); + config.put("key", 3); + + thrown.expect(IllegalArgumentException.class); + builder.defaultServiceConfig(config); + } + + @Test + public void defaultServiceConfig_nested() { + Builder builder = new Builder("target"); + Map config = new HashMap<>(); + List list1 = new ArrayList<>(); + list1.add(123D); + list1.add(null); + list1.add(true); + list1.add("str"); + Map map2 = new HashMap<>(); + map2.put("key2", false); + map2.put("key3", null); + map2.put("key4", Collections.singletonList("v4")); + map2.put("key4", 3.14D); + map2.put("key5", new HashMap()); + list1.add(map2); + config.put("key1", list1); + + builder.defaultServiceConfig(config); + + assertThat(builder.defaultServiceConfig).containsExactlyEntriesIn(config); + } + + @Test + public void disableNameResolverServiceConfig() { + Builder builder = new Builder("target"); + assertThat(builder.lookUpServiceConfig).isTrue(); + + builder.lookUpServiceConfig(false); + assertThat(builder.lookUpServiceConfig).isFalse(); + } + static class Builder extends AbstractManagedChannelImplBuilder { Builder(String target) { super(target); diff --git a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java index 14d72f8eebc..9f3e97a1163 100644 --- a/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java +++ b/core/src/test/java/io/grpc/internal/ManagedChannelImplTest.java @@ -3409,7 +3409,7 @@ public void nameResolverHelper_emptyConfigSucceeds() { @Test public void nameResolverHelper_badConfigFails() { int defaultPort = 1; - ProxyDetector proxyDetector = GrpcUtil.getDefaultProxyDetector(); + ProxyDetector proxyDetector = GrpcUtil.getDefaultProxyDetector(); SynchronizationContext syncCtx = new SynchronizationContext(Thread.currentThread().getUncaughtExceptionHandler()); boolean retryEnabled = false; @@ -3433,6 +3433,271 @@ public void nameResolverHelper_badConfigFails() { assertThat(coe.getError().getCause()).isInstanceOf(ClassCastException.class); } + @Test + public void disableServiceConfigLookUp_noDefaultConfig() throws Exception { + LoadBalancerRegistry.getDefaultRegistry().register(mockLoadBalancerProvider); + try { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + channelBuilder.lookUpServiceConfig(false); + + Map serviceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":true}]}"); + Attributes serviceConfigAttrs = + Attributes.newBuilder() + .set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig) + .build(); + nameResolverFactory.nextResolvedAttributes.set(serviceConfigAttrs); + + createChannel(); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(mockLoadBalancer).handleResolvedAddressGroups( + eq(ImmutableList.of(addressGroup)), + attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG)) + .isNull(); + verify(mockLoadBalancer, never()).handleNameResolutionError(any(Status.class)); + } finally { + LoadBalancerRegistry.getDefaultRegistry().deregister(mockLoadBalancerProvider); + } + } + + @Test + public void disableServiceConfigLookUp_withDefaultConfig() throws Exception { + LoadBalancerRegistry.getDefaultRegistry().register(mockLoadBalancerProvider); + try { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + channelBuilder.lookUpServiceConfig(false); + Map defaultServiceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":true}]}"); + channelBuilder.defaultServiceConfig(defaultServiceConfig); + + Map serviceConfig = new HashMap<>(); + Attributes serviceConfigAttrs = + Attributes.newBuilder() + .set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig) + .build(); + nameResolverFactory.nextResolvedAttributes.set(serviceConfigAttrs); + + createChannel(); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(mockLoadBalancer).handleResolvedAddressGroups( + eq(ImmutableList.of(addressGroup)), + attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG)) + .isEqualTo(defaultServiceConfig); + verify(mockLoadBalancer, never()).handleNameResolutionError(any(Status.class)); + } finally { + LoadBalancerRegistry.getDefaultRegistry().deregister(mockLoadBalancerProvider); + } + } + + @Test + public void enableServiceConfigLookUp_noDefaultConfig() throws Exception { + LoadBalancerRegistry.getDefaultRegistry().register(mockLoadBalancerProvider); + try { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + + Map serviceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":true}]}"); + Attributes serviceConfigAttrs = + Attributes.newBuilder() + .set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig) + .build(); + nameResolverFactory.nextResolvedAttributes.set(serviceConfigAttrs); + + createChannel(); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(mockLoadBalancer).handleResolvedAddressGroups( + eq(ImmutableList.of(addressGroup)), + attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG)) + .isEqualTo(serviceConfig); + verify(mockLoadBalancer, never()).handleNameResolutionError(any(Status.class)); + + // new config + serviceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":false}]}"); + serviceConfigAttrs = + Attributes.newBuilder() + .set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig) + .build(); + nameResolverFactory.nextResolvedAttributes.set(serviceConfigAttrs); + nameResolverFactory.allResolved(); + + attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(mockLoadBalancer, times(2)).handleResolvedAddressGroups( + eq(ImmutableList.of(addressGroup)), + attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG)) + .isEqualTo(serviceConfig); + verify(mockLoadBalancer, never()).handleNameResolutionError(any(Status.class)); + } finally { + LoadBalancerRegistry.getDefaultRegistry().deregister(mockLoadBalancerProvider); + } + } + + @Test + public void enableServiceConfigLookUp_withDefaultConfig() throws Exception { + LoadBalancerRegistry.getDefaultRegistry().register(mockLoadBalancerProvider); + try { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + Map defaultServiceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":true}]}"); + channelBuilder.defaultServiceConfig(defaultServiceConfig); + + Map serviceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService2\"}]," + + "\"waitForReady\":false}]}"); + Attributes serviceConfigAttrs = + Attributes.newBuilder() + .set(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG, serviceConfig) + .build(); + nameResolverFactory.nextResolvedAttributes.set(serviceConfigAttrs); + + createChannel(); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(mockLoadBalancer).handleResolvedAddressGroups( + eq(ImmutableList.of(addressGroup)), + attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG)) + .isEqualTo(serviceConfig); + verify(mockLoadBalancer, never()).handleNameResolutionError(any(Status.class)); + } finally { + LoadBalancerRegistry.getDefaultRegistry().deregister(mockLoadBalancerProvider); + } + } + + @Test + public void enableServiceConfigLookUp_resolverReturnsNoConfig_withDefaultConfig() + throws Exception { + LoadBalancerRegistry.getDefaultRegistry().register(mockLoadBalancerProvider); + try { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + Map defaultServiceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":true}]}"); + channelBuilder.defaultServiceConfig(defaultServiceConfig); + + Attributes serviceConfigAttrs = Attributes.EMPTY; + nameResolverFactory.nextResolvedAttributes.set(serviceConfigAttrs); + + createChannel(); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(mockLoadBalancer).handleResolvedAddressGroups( + eq(ImmutableList.of(addressGroup)), + attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG)) + .isEqualTo(defaultServiceConfig); + verify(mockLoadBalancer, never()).handleNameResolutionError(any(Status.class)); + } finally { + LoadBalancerRegistry.getDefaultRegistry().deregister(mockLoadBalancerProvider); + } + } + + @Test + public void enableServiceConfigLookUp_resolverReturnsNoConfig_noDefaultConfig() { + LoadBalancerRegistry.getDefaultRegistry().register(mockLoadBalancerProvider); + try { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + + Attributes serviceConfigAttrs = Attributes.EMPTY; + nameResolverFactory.nextResolvedAttributes.set(serviceConfigAttrs); + + createChannel(); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(mockLoadBalancer).handleResolvedAddressGroups( + eq(ImmutableList.of(addressGroup)), + attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG)) + .isNull(); + verify(mockLoadBalancer, never()).handleNameResolutionError(any(Status.class)); + } finally { + LoadBalancerRegistry.getDefaultRegistry().deregister(mockLoadBalancerProvider); + } + } + + @Test + public void useDefaultImmediatelyIfDisableLookUp() throws Exception { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + channelBuilder.lookUpServiceConfig(false); + Map defaultServiceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":true}]}"); + channelBuilder.defaultServiceConfig(defaultServiceConfig); + requestConnection = false; + channelBuilder.maxTraceEvents(10); + + createChannel(); + + int size = getStats(channel).channelTrace.events.size(); + assertThat(getStats(channel).channelTrace.events.get(size - 1)) + .isEqualTo(new ChannelTrace.Event.Builder() + .setDescription("Service config look-up disabled, using default service config") + .setSeverity(ChannelTrace.Event.Severity.CT_INFO) + .setTimestampNanos(timer.getTicker().read()) + .build()); + } + + @Test + public void notUseDefaultImmediatelyIfEnableLookUp() throws Exception { + FakeNameResolverFactory nameResolverFactory = + new FakeNameResolverFactory.Builder(expectedUri) + .setServers(ImmutableList.of(addressGroup)).build(); + channelBuilder.nameResolverFactory(nameResolverFactory); + channelBuilder.lookUpServiceConfig(true); + Map defaultServiceConfig = + parseConfig("{\"methodConfig\":[{" + + "\"name\":[{\"service\":\"SimpleService1\"}]," + + "\"waitForReady\":true}]}"); + channelBuilder.defaultServiceConfig(defaultServiceConfig); + requestConnection = false; + channelBuilder.maxTraceEvents(10); + + createChannel(); + + int size = getStats(channel).channelTrace.events.size(); + assertThat(getStats(channel).channelTrace.events.get(size - 1)) + .isNotEqualTo(new ChannelTrace.Event.Builder() + .setDescription("Using default service config") + .setSeverity(ChannelTrace.Event.Severity.CT_INFO) + .setTimestampNanos(timer.getTicker().read()) + .build()); + } + private static final class ChannelBuilder extends AbstractManagedChannelImplBuilder { diff --git a/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java b/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java index 8627b26f4b6..c0f431a3fa8 100644 --- a/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java +++ b/core/src/test/java/io/grpc/internal/ServiceConfigInterceptorTest.java @@ -101,6 +101,21 @@ public void withWaitForReady() { assertThat(callOptionsCap.getValue().isWaitForReady()).isTrue(); } + @Test + public void handleNullConfig() { + JsonObj name = new JsonObj("service", "service"); + JsonObj methodConfig = new JsonObj("name", new JsonList(name), "waitForReady", true); + JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig)); + + interceptor.handleUpdate(serviceConfig); + interceptor.handleUpdate(null); + + interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT.withoutWaitForReady(), channel); + + verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture()); + assertThat(callOptionsCap.getValue().isWaitForReady()).isFalse(); + } + @Test public void handleUpdateNotCalledBeforeInterceptCall() { interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT.withoutWaitForReady(), channel);