mirror of https://github.com/grpc/grpc-java.git
core: apply service config
This commit is contained in:
parent
49c8bdb60a
commit
2df76cc710
|
|
@ -164,7 +164,7 @@ final class AutoConfiguredLoadBalancerFactory extends LoadBalancer.Factory {
|
||||||
}
|
}
|
||||||
|
|
||||||
String serviceConfigChoiceBalancingPolicy =
|
String serviceConfigChoiceBalancingPolicy =
|
||||||
ServiceConfigUtil.getLoadBalancingPolicy(config);
|
ServiceConfigUtil.getLoadBalancingPolicyFromServiceConfig(config);
|
||||||
|
|
||||||
// Check for an explicitly present lb choice
|
// Check for an explicitly present lb choice
|
||||||
if (serviceConfigChoiceBalancingPolicy != null) {
|
if (serviceConfigChoiceBalancingPolicy != null) {
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,12 @@ import java.net.UnknownHostException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
@ -63,6 +66,22 @@ final class DnsNameResolver extends NameResolver {
|
||||||
|
|
||||||
private static final boolean JNDI_AVAILABLE = jndiAvailable();
|
private static final boolean JNDI_AVAILABLE = jndiAvailable();
|
||||||
|
|
||||||
|
private static final String SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY = "clientLanguage";
|
||||||
|
private static final String SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY = "percentage";
|
||||||
|
private static final String SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY = "clientHostname";
|
||||||
|
private static final String SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY = "serviceConfig";
|
||||||
|
|
||||||
|
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
|
||||||
|
static final String SERVICE_CONFIG_PREFIX = "_grpc_config=";
|
||||||
|
private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS =
|
||||||
|
Collections.unmodifiableSet(
|
||||||
|
new HashSet<String>(
|
||||||
|
Arrays.asList(
|
||||||
|
SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY,
|
||||||
|
SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY,
|
||||||
|
SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY,
|
||||||
|
SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY)));
|
||||||
|
|
||||||
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
|
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
|
||||||
private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
|
private static final String SERVICE_CONFIG_NAME_PREFIX = "_grpc_config.";
|
||||||
// From https://github.com/grpc/proposal/blob/master/A5-grpclb-in-dns.md
|
// From https://github.com/grpc/proposal/blob/master/A5-grpclb-in-dns.md
|
||||||
|
|
@ -196,11 +215,10 @@ final class DnsNameResolver extends NameResolver {
|
||||||
Map<String, Object> serviceConfig = null;
|
Map<String, Object> serviceConfig = null;
|
||||||
try {
|
try {
|
||||||
for (Map<String, Object> possibleConfig :
|
for (Map<String, Object> possibleConfig :
|
||||||
ServiceConfigUtil.parseTxtResults(resolvedInetAddrs.txtRecords)) {
|
parseTxtResults(resolvedInetAddrs.txtRecords)) {
|
||||||
try {
|
try {
|
||||||
serviceConfig =
|
serviceConfig =
|
||||||
ServiceConfigUtil.maybeChooseServiceConfig(
|
maybeChooseServiceConfig(possibleConfig, random, getLocalHostname());
|
||||||
possibleConfig, random, getLocalHostname());
|
|
||||||
} catch (RuntimeException e) {
|
} catch (RuntimeException e) {
|
||||||
logger.log(Level.WARNING, "Bad service config choice " + possibleConfig, e);
|
logger.log(Level.WARNING, "Bad service config choice " + possibleConfig, e);
|
||||||
}
|
}
|
||||||
|
|
@ -265,6 +283,120 @@ final class DnsNameResolver extends NameResolver {
|
||||||
this.delegateResolver = delegateResolver;
|
this.delegateResolver = delegateResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@VisibleForTesting
|
||||||
|
static List<Map<String, Object>> parseTxtResults(List<String> txtRecords) {
|
||||||
|
List<Map<String, Object>> serviceConfigs = new ArrayList<Map<String, Object>>();
|
||||||
|
for (String txtRecord : txtRecords) {
|
||||||
|
if (txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
|
||||||
|
List<Map<String, Object>> choices;
|
||||||
|
try {
|
||||||
|
Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
|
||||||
|
if (!(rawChoices instanceof List)) {
|
||||||
|
throw new IOException("wrong type " + rawChoices);
|
||||||
|
}
|
||||||
|
List<Object> listChoices = (List<Object>) rawChoices;
|
||||||
|
for (Object obj : listChoices) {
|
||||||
|
if (!(obj instanceof Map)) {
|
||||||
|
throw new IOException("wrong element type " + rawChoices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
choices = (List<Map<String, Object>>) (List<?>) listChoices;
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.log(Level.WARNING, "Bad service config: " + txtRecord, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
serviceConfigs.addAll(choices);
|
||||||
|
} else {
|
||||||
|
logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serviceConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static final Double getPercentageFromChoice(
|
||||||
|
Map<String, Object> serviceConfigChoice) {
|
||||||
|
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ServiceConfigUtil.getDouble(serviceConfigChoice, SERVICE_CONFIG_CHOICE_PERCENTAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static final List<String> getClientLanguagesFromChoice(
|
||||||
|
Map<String, Object> serviceConfigChoice) {
|
||||||
|
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ServiceConfigUtil.checkStringList(
|
||||||
|
ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_LANGUAGE_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static final List<String> getHostnamesFromChoice(
|
||||||
|
Map<String, Object> serviceConfigChoice) {
|
||||||
|
if (!serviceConfigChoice.containsKey(SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ServiceConfigUtil.checkStringList(
|
||||||
|
ServiceConfigUtil.getList(serviceConfigChoice, SERVICE_CONFIG_CHOICE_CLIENT_HOSTNAME_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a given Service Config choice applies, and if so, returns it.
|
||||||
|
*
|
||||||
|
* @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
|
||||||
|
* Service Config in DNS</a>
|
||||||
|
* @param choice The service config choice.
|
||||||
|
* @return The service config object or {@code null} if this choice does not apply.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
@SuppressWarnings("BetaApi") // Verify isn't all that beta
|
||||||
|
@VisibleForTesting
|
||||||
|
static Map<String, Object> maybeChooseServiceConfig(
|
||||||
|
Map<String, Object> choice, Random random, String hostname) {
|
||||||
|
for (Entry<String, ?> entry : choice.entrySet()) {
|
||||||
|
Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> clientLanguages = getClientLanguagesFromChoice(choice);
|
||||||
|
if (clientLanguages != null && clientLanguages.size() != 0) {
|
||||||
|
boolean javaPresent = false;
|
||||||
|
for (String lang : clientLanguages) {
|
||||||
|
if ("java".equalsIgnoreCase(lang)) {
|
||||||
|
javaPresent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!javaPresent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Double percentage = getPercentageFromChoice(choice);
|
||||||
|
if (percentage != null) {
|
||||||
|
int pct = percentage.intValue();
|
||||||
|
Verify.verify(pct >= 0 && pct <= 100, "Bad percentage: %s", percentage);
|
||||||
|
if (random.nextInt(100) >= pct) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> clientHostnames = getHostnamesFromChoice(choice);
|
||||||
|
if (clientHostnames != null && clientHostnames.size() != 0) {
|
||||||
|
boolean hostnamePresent = false;
|
||||||
|
for (String clientHostname : clientHostnames) {
|
||||||
|
if (clientHostname.equals(hostname)) {
|
||||||
|
hostnamePresent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hostnamePresent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ServiceConfigUtil.getObject(choice, SERVICE_CONFIG_CHOICE_SERVICE_CONFIG_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether the JNDI DNS resolver is available. This is accomplished by looking up a
|
* Returns whether the JNDI DNS resolver is available. This is accomplished by looking up a
|
||||||
* particular class. It is believed to be the default (only?) DNS resolver that will actually be
|
* particular class. It is believed to be the default (only?) DNS resolver that will actually be
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
|
@ -134,6 +135,8 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
|
||||||
|
|
||||||
private final ConnectivityStateManager channelStateManager = new ConnectivityStateManager();
|
private final ConnectivityStateManager channelStateManager = new ConnectivityStateManager();
|
||||||
|
|
||||||
|
private final ServiceConfigInterceptor serviceConfigInterceptor = new ServiceConfigInterceptor();
|
||||||
|
|
||||||
private final BackoffPolicy.Provider backoffPolicyProvider;
|
private final BackoffPolicy.Provider backoffPolicyProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -537,6 +540,7 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
|
||||||
this.transportFactory =
|
this.transportFactory =
|
||||||
new CallCredentialsApplyingTransportFactory(clientTransportFactory, this.executor);
|
new CallCredentialsApplyingTransportFactory(clientTransportFactory, this.executor);
|
||||||
Channel channel = new RealChannel();
|
Channel channel = new RealChannel();
|
||||||
|
channel = ClientInterceptors.intercept(channel, serviceConfigInterceptor);
|
||||||
if (builder.binlogProvider != null) {
|
if (builder.binlogProvider != null) {
|
||||||
channel = builder.binlogProvider.wrapChannel(channel);
|
channel = builder.binlogProvider.wrapChannel(channel);
|
||||||
}
|
}
|
||||||
|
|
@ -1161,7 +1165,11 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
|
||||||
|
|
||||||
nameResolverBackoffPolicy = null;
|
nameResolverBackoffPolicy = null;
|
||||||
|
|
||||||
|
Map<String, Object> serviceConfig =
|
||||||
|
config.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
serviceConfigInterceptor.handleUpdate(serviceConfig);
|
||||||
if (retryEnabled) {
|
if (retryEnabled) {
|
||||||
retryPolicies = getRetryPolicies(config);
|
retryPolicies = getRetryPolicies(config);
|
||||||
throttle = getThrottle(config);
|
throttle = getThrottle(config);
|
||||||
|
|
@ -1223,7 +1231,7 @@ final class ManagedChannelImpl extends ManagedChannel implements Instrumented<Ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(zdapeng): test retryEnabled = true/flase really works as expected.
|
// TODO(zdapeng): test retryEnabled = true/flase really works as expected
|
||||||
private RetryPolicies getRetryPolicies(Attributes config) {
|
private RetryPolicies getRetryPolicies(Attributes config) {
|
||||||
return ServiceConfigUtil.getRetryPolicies(
|
return ServiceConfigUtil.getRetryPolicies(
|
||||||
config.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG), maxRetryAttempts);
|
config.get(GrpcAttributes.NAME_RESOLVER_SERVICE_CONFIG), maxRetryAttempts);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018, 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.base.Preconditions.checkArgument;
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import io.grpc.CallOptions;
|
||||||
|
import io.grpc.Channel;
|
||||||
|
import io.grpc.ClientCall;
|
||||||
|
import io.grpc.ClientInterceptor;
|
||||||
|
import io.grpc.Deadline;
|
||||||
|
import io.grpc.MethodDescriptor;
|
||||||
|
import io.grpc.Status.Code;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies RPCs in in conformance with a Service Config.
|
||||||
|
*/
|
||||||
|
final class ServiceConfigInterceptor implements ClientInterceptor {
|
||||||
|
|
||||||
|
private static final Logger logger = Logger.getLogger(ServiceConfigInterceptor.class.getName());
|
||||||
|
|
||||||
|
// Map from method name to MethodInfo
|
||||||
|
@VisibleForTesting
|
||||||
|
final AtomicReference<Map<String, MethodInfo>> serviceMethodMap
|
||||||
|
= new AtomicReference<Map<String, MethodInfo>>();
|
||||||
|
@VisibleForTesting
|
||||||
|
final AtomicReference<Map<String, MethodInfo>> serviceMap
|
||||||
|
= new AtomicReference<Map<String, MethodInfo>>();
|
||||||
|
|
||||||
|
ServiceConfigInterceptor() {}
|
||||||
|
|
||||||
|
void handleUpdate(Map<String, Object> serviceConfig) {
|
||||||
|
Map<String, MethodInfo> newServiceMethodConfigs = new HashMap<String, MethodInfo>();
|
||||||
|
Map<String, MethodInfo> newServiceConfigs = new HashMap<String, MethodInfo>();
|
||||||
|
|
||||||
|
// Try and do as much validation here before we swap out the existing configuration. In case
|
||||||
|
// the input is invalid, we don't want to lose the existing configuration.
|
||||||
|
|
||||||
|
List<Map<String, Object>> methodConfigs =
|
||||||
|
ServiceConfigUtil.getMethodConfigFromServiceConfig(serviceConfig);
|
||||||
|
if (methodConfigs == null) {
|
||||||
|
logger.log(Level.FINE, "No method configs found, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map<String, Object> methodConfig : methodConfigs) {
|
||||||
|
MethodInfo info = new MethodInfo(methodConfig);
|
||||||
|
|
||||||
|
List<Map<String, Object>> nameList =
|
||||||
|
ServiceConfigUtil.getNameListFromMethodConfig(methodConfig);
|
||||||
|
|
||||||
|
checkArgument(
|
||||||
|
nameList != null && !nameList.isEmpty(), "no names in method config %s", methodConfig);
|
||||||
|
for (Map<String, Object> name : nameList) {
|
||||||
|
String serviceName = ServiceConfigUtil.getServiceFromName(name);
|
||||||
|
checkArgument(!Strings.isNullOrEmpty(serviceName), "missing service name");
|
||||||
|
String methodName = ServiceConfigUtil.getMethodFromName(name);
|
||||||
|
if (Strings.isNullOrEmpty(methodName)) {
|
||||||
|
// Service scoped config
|
||||||
|
checkArgument(
|
||||||
|
!newServiceConfigs.containsKey(serviceName), "Duplicate service %s", serviceName);
|
||||||
|
newServiceConfigs.put(serviceName, info);
|
||||||
|
} else {
|
||||||
|
// Method scoped config
|
||||||
|
String fullMethodName = MethodDescriptor.generateFullMethodName(serviceName, methodName);
|
||||||
|
checkArgument(
|
||||||
|
!newServiceMethodConfigs.containsKey(fullMethodName),
|
||||||
|
"Duplicate method name %s",
|
||||||
|
fullMethodName);
|
||||||
|
newServiceMethodConfigs.put(fullMethodName, info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Okay, service config is good, swap it.
|
||||||
|
serviceMethodMap.set(Collections.unmodifiableMap(newServiceMethodConfigs));
|
||||||
|
serviceMap.set(Collections.unmodifiableMap(newServiceConfigs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equivalent of MethodConfig from a ServiceConfig.
|
||||||
|
*/
|
||||||
|
static final class MethodInfo {
|
||||||
|
final Long timeoutNanos;
|
||||||
|
final Boolean waitForReady;
|
||||||
|
final Integer maxInboundMessageSize;
|
||||||
|
final Integer maxOutboundMessageSize;
|
||||||
|
final RetryPolicy retryPolicy;
|
||||||
|
|
||||||
|
MethodInfo(Map<String, Object> methodConfig) {
|
||||||
|
timeoutNanos = ServiceConfigUtil.getTimeoutFromMethodConfig(methodConfig);
|
||||||
|
waitForReady = ServiceConfigUtil.getWaitForReadyFromMethodConfig(methodConfig);
|
||||||
|
maxInboundMessageSize =
|
||||||
|
ServiceConfigUtil.getMaxResponseMessageBytesFromMethodConfig(methodConfig);
|
||||||
|
if (maxInboundMessageSize != null) {
|
||||||
|
checkArgument(
|
||||||
|
maxInboundMessageSize >= 0,
|
||||||
|
"maxInboundMessageSize %s exceeds bounds", maxInboundMessageSize);
|
||||||
|
}
|
||||||
|
maxOutboundMessageSize =
|
||||||
|
ServiceConfigUtil.getMaxRequestMessageBytesFromMethodConfig(methodConfig);
|
||||||
|
if (maxOutboundMessageSize != null) {
|
||||||
|
checkArgument(
|
||||||
|
maxOutboundMessageSize >= 0,
|
||||||
|
"maxOutboundMessageSize %s exceeds bounds", maxOutboundMessageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> policy = ServiceConfigUtil.getRetryPolicyFromMethodConfig(methodConfig);
|
||||||
|
retryPolicy = policy == null ? null : new RetryPolicy(policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(
|
||||||
|
timeoutNanos, waitForReady, maxInboundMessageSize, maxOutboundMessageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof MethodInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
MethodInfo that = (MethodInfo) other;
|
||||||
|
return Objects.equal(this.timeoutNanos, that.timeoutNanos)
|
||||||
|
&& Objects.equal(this.waitForReady, that.waitForReady)
|
||||||
|
&& Objects.equal(this.maxInboundMessageSize, that.maxInboundMessageSize)
|
||||||
|
&& Objects.equal(this.maxOutboundMessageSize, that.maxOutboundMessageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("timeoutNanos", timeoutNanos)
|
||||||
|
.add("waitForReady", waitForReady)
|
||||||
|
.add("maxInboundMessageSize", maxInboundMessageSize)
|
||||||
|
.add("maxOutboundMessageSize", maxOutboundMessageSize)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class RetryPolicy {
|
||||||
|
final int maxAttempts;
|
||||||
|
final long initialBackoffNanos;
|
||||||
|
final long maxBackoffNanos;
|
||||||
|
final double backoffMultiplier;
|
||||||
|
final Set<Code> retryableStatusCodes;
|
||||||
|
|
||||||
|
RetryPolicy(Map<String, Object> retryPolicy) {
|
||||||
|
maxAttempts = checkNotNull(
|
||||||
|
ServiceConfigUtil.getMaxAttemptsFromRetryPolicy(retryPolicy),
|
||||||
|
"maxAttempts cannot be empty");
|
||||||
|
checkArgument(maxAttempts >= 2, "maxAttempts must be greater than 1: %s", maxAttempts);
|
||||||
|
|
||||||
|
initialBackoffNanos = checkNotNull(
|
||||||
|
ServiceConfigUtil.getInitialBackoffNanosFromRetryPolicy(retryPolicy),
|
||||||
|
"initialBackoff cannot be empty");
|
||||||
|
checkArgument(
|
||||||
|
initialBackoffNanos > 0,
|
||||||
|
"initialBackoffNanos must be greater than 0: %s",
|
||||||
|
initialBackoffNanos);
|
||||||
|
|
||||||
|
maxBackoffNanos = checkNotNull(
|
||||||
|
ServiceConfigUtil.getMaxBackoffNanosFromRetryPolicy(retryPolicy),
|
||||||
|
"maxBackoff cannot be empty");
|
||||||
|
checkArgument(
|
||||||
|
maxBackoffNanos > 0, "maxBackoff must be greater than 0: %s", maxBackoffNanos);
|
||||||
|
|
||||||
|
backoffMultiplier = checkNotNull(
|
||||||
|
ServiceConfigUtil.getBackoffMultiplierFromRetryPolicy(retryPolicy),
|
||||||
|
"backoffMultiplier cannot be empty");
|
||||||
|
checkArgument(
|
||||||
|
backoffMultiplier > 0,
|
||||||
|
"backoffMultiplier must be greater than 0: %s",
|
||||||
|
backoffMultiplier);
|
||||||
|
|
||||||
|
List<String> rawCodes =
|
||||||
|
ServiceConfigUtil.getRetryableStatusCodesFromRetryPolicy(retryPolicy);
|
||||||
|
checkNotNull(rawCodes, "rawCodes must be present");
|
||||||
|
checkArgument(!rawCodes.isEmpty(), "rawCodes can't be empty");
|
||||||
|
EnumSet<Code> codes = EnumSet.noneOf(Code.class);
|
||||||
|
// service config doesn't say if duplicates are allowed, so just accept them.
|
||||||
|
for (String rawCode : rawCodes) {
|
||||||
|
codes.add(Code.valueOf(rawCode));
|
||||||
|
}
|
||||||
|
retryableStatusCodes = Collections.unmodifiableSet(codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
RetryPolicy(
|
||||||
|
int maxAttempts,
|
||||||
|
long initialBackoffNanos,
|
||||||
|
long maxBackoffNanos,
|
||||||
|
double backoffMultiplier,
|
||||||
|
Set<Code> retryableStatusCodes) {
|
||||||
|
this.maxAttempts = maxAttempts;
|
||||||
|
this.initialBackoffNanos = initialBackoffNanos;
|
||||||
|
this.maxBackoffNanos = maxBackoffNanos;
|
||||||
|
this.backoffMultiplier = backoffMultiplier;
|
||||||
|
this.retryableStatusCodes = retryableStatusCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(
|
||||||
|
maxAttempts,
|
||||||
|
initialBackoffNanos,
|
||||||
|
maxBackoffNanos,
|
||||||
|
backoffMultiplier,
|
||||||
|
retryableStatusCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (!(other instanceof RetryPolicy)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RetryPolicy that = (RetryPolicy) other;
|
||||||
|
return Objects.equal(this.maxAttempts, that.maxAttempts)
|
||||||
|
&& Objects.equal(this.initialBackoffNanos, that.initialBackoffNanos)
|
||||||
|
&& Objects.equal(this.maxBackoffNanos, that.maxBackoffNanos)
|
||||||
|
&& Objects.equal(this.backoffMultiplier, that.backoffMultiplier)
|
||||||
|
&& Objects.equal(this.retryableStatusCodes, that.retryableStatusCodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("maxAttempts", maxAttempts)
|
||||||
|
.add("initialBackoffNanos", initialBackoffNanos)
|
||||||
|
.add("maxBackoffNanos", maxBackoffNanos)
|
||||||
|
.add("backoffMultiplier", backoffMultiplier)
|
||||||
|
.add("retryableStatusCodes", retryableStatusCodes)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final CallOptions.Key<MethodInfo.RetryPolicy> RETRY_POLICY_KEY =
|
||||||
|
CallOptions.Key.of("internal-retry-policy", null);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
|
||||||
|
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
|
||||||
|
Map<String, MethodInfo> localServiceMethodMap = serviceMethodMap.get();
|
||||||
|
MethodInfo info = null;
|
||||||
|
if (localServiceMethodMap != null) {
|
||||||
|
info = localServiceMethodMap.get(method.getFullMethodName());
|
||||||
|
}
|
||||||
|
if (info == null) {
|
||||||
|
Map<String, MethodInfo> localServiceMap = serviceMap.get();
|
||||||
|
if (localServiceMap != null) {
|
||||||
|
info = localServiceMap.get(
|
||||||
|
MethodDescriptor.extractFullServiceName(method.getFullMethodName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info == null) {
|
||||||
|
return next.newCall(method, callOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.timeoutNanos != null) {
|
||||||
|
Deadline newDeadline = Deadline.after(info.timeoutNanos, TimeUnit.NANOSECONDS);
|
||||||
|
Deadline existingDeadline = callOptions.getDeadline();
|
||||||
|
// If the new deadline is sooner than the existing deadline, swap them.
|
||||||
|
if (existingDeadline == null || newDeadline.compareTo(existingDeadline) < 0) {
|
||||||
|
callOptions = callOptions.withDeadline(newDeadline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.waitForReady != null) {
|
||||||
|
callOptions =
|
||||||
|
info.waitForReady ? callOptions.withWaitForReady() : callOptions.withoutWaitForReady();
|
||||||
|
}
|
||||||
|
if (info.maxInboundMessageSize != null) {
|
||||||
|
Integer existingLimit = callOptions.getMaxInboundMessageSize();
|
||||||
|
if (existingLimit != null) {
|
||||||
|
callOptions = callOptions.withMaxInboundMessageSize(
|
||||||
|
Math.min(existingLimit, info.maxInboundMessageSize));
|
||||||
|
} else {
|
||||||
|
callOptions = callOptions.withMaxInboundMessageSize(info.maxInboundMessageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.maxOutboundMessageSize != null) {
|
||||||
|
Integer existingLimit = callOptions.getMaxOutboundMessageSize();
|
||||||
|
if (existingLimit != null) {
|
||||||
|
callOptions = callOptions.withMaxOutboundMessageSize(
|
||||||
|
Math.min(existingLimit, info.maxOutboundMessageSize));
|
||||||
|
} else {
|
||||||
|
callOptions = callOptions.withMaxOutboundMessageSize(info.maxOutboundMessageSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (info.retryPolicy != null) {
|
||||||
|
callOptions = callOptions.withOption(RETRY_POLICY_KEY, info.retryPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.newCall(method, callOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,26 +18,20 @@ package io.grpc.internal;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
import static com.google.common.base.Preconditions.checkState;
|
||||||
|
import static com.google.common.math.LongMath.checkedAdd;
|
||||||
|
|
||||||
import com.google.common.base.Verify;
|
|
||||||
import io.grpc.MethodDescriptor;
|
import io.grpc.MethodDescriptor;
|
||||||
import io.grpc.Status.Code;
|
import io.grpc.Status.Code;
|
||||||
import io.grpc.internal.RetriableStream.RetryPolicies;
|
import io.grpc.internal.RetriableStream.RetryPolicies;
|
||||||
import io.grpc.internal.RetriableStream.RetryPolicy;
|
import io.grpc.internal.RetriableStream.RetryPolicy;
|
||||||
import io.grpc.internal.RetriableStream.Throttle;
|
import io.grpc.internal.RetriableStream.Throttle;
|
||||||
import java.io.IOException;
|
import java.text.ParseException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.Random;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.logging.Level;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
|
@ -48,24 +42,29 @@ final class ServiceConfigUtil {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(ServiceConfigUtil.class.getName());
|
private static final Logger logger = Logger.getLogger(ServiceConfigUtil.class.getName());
|
||||||
|
|
||||||
// From https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md
|
private static final String SERVICE_CONFIG_METHOD_CONFIG_KEY = "methodConfig";
|
||||||
static final String SERVICE_CONFIG_PREFIX = "_grpc_config=";
|
private static final String SERVICE_CONFIG_LOAD_BALANCING_POLICY_key = "loadBalancingPolicy";
|
||||||
private static final Set<String> SERVICE_CONFIG_CHOICE_KEYS =
|
private static final String METHOD_CONFIG_NAME_KEY = "name";
|
||||||
Collections.unmodifiableSet(
|
private static final String METHOD_CONFIG_TIMEOUT_KEY = "timeout";
|
||||||
new HashSet<String>(
|
private static final String METHOD_CONFIG_WAIT_FOR_READY_KEY = "waitForReady";
|
||||||
Arrays.asList("clientLanguage", "percentage", "clientHostname", "serviceConfig")));
|
private static final String METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY =
|
||||||
|
"maxRequestMessageBytes";
|
||||||
|
private static final String METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY =
|
||||||
|
"maxResponseMessageBytes";
|
||||||
|
private static final String METHOD_CONFIG_RETRY_POLICY_KEY = "retryPolicy";
|
||||||
|
private static final String NAME_SERVICE_KEY = "service";
|
||||||
|
private static final String NAME_METHOD_KEY = "method";
|
||||||
|
private static final String RETRY_POLICY_MAX_ATTEMPTS_KEY = "maxAttempts";
|
||||||
|
private static final String RETRY_POLICY_INITIAL_BACKOFF_KEY = "initialBackoff";
|
||||||
|
private static final String RETRY_POLICY_MAX_BACKOFF_KEY = "maxBackoff";
|
||||||
|
private static final String RETRY_POLICY_BACKOFF_MULTIPLIER_KEY = "backoffMultiplier";
|
||||||
|
private static final String RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY = "retryableStatusCodes";
|
||||||
|
|
||||||
|
private static final long DURATION_SECONDS_MIN = -315576000000L;
|
||||||
|
private static final long DURATION_SECONDS_MAX = 315576000000L;
|
||||||
|
|
||||||
private ServiceConfigUtil() {}
|
private ServiceConfigUtil() {}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
static String getLoadBalancingPolicy(Map<String, Object> serviceConfig) {
|
|
||||||
String key = "loadBalancingPolicy";
|
|
||||||
if (serviceConfig.containsKey(key)) {
|
|
||||||
return getString(serviceConfig, key);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets retry policies from the service config.
|
* Gets retry policies from the service config.
|
||||||
*
|
*
|
||||||
|
|
@ -94,10 +93,10 @@ final class ServiceConfigUtil {
|
||||||
"maxBackoff": string, // Long decimal with "s" appended
|
"maxBackoff": string, // Long decimal with "s" appended
|
||||||
"backoffMultiplier": number
|
"backoffMultiplier": number
|
||||||
"retryableStatusCodes": []
|
"retryableStatusCodes": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (serviceConfig.containsKey("methodConfig")) {
|
if (serviceConfig.containsKey("methodConfig")) {
|
||||||
|
|
@ -204,98 +203,152 @@ final class ServiceConfigUtil {
|
||||||
return new Throttle(maxTokens, tokenRatio);
|
return new Throttle(maxTokens, tokenRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if a given Service Config choice applies, and if so, returns it.
|
|
||||||
*
|
|
||||||
* @see <a href="https://github.com/grpc/proposal/blob/master/A2-service-configs-in-dns.md">
|
|
||||||
* Service Config in DNS</a>
|
|
||||||
* @param choice The service config choice.
|
|
||||||
* @return The service config object or {@code null} if this choice does not apply.
|
|
||||||
*/
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@SuppressWarnings("BetaApi") // Verify isn't all that beta
|
static Integer getMaxAttemptsFromRetryPolicy(Map<String, Object> retryPolicy) {
|
||||||
static Map<String, Object> maybeChooseServiceConfig(
|
if (!retryPolicy.containsKey(RETRY_POLICY_MAX_ATTEMPTS_KEY)) {
|
||||||
Map<String, Object> choice, Random random, String hostname) {
|
return null;
|
||||||
for (Entry<String, ?> entry : choice.entrySet()) {
|
|
||||||
Verify.verify(SERVICE_CONFIG_CHOICE_KEYS.contains(entry.getKey()), "Bad key: %s", entry);
|
|
||||||
}
|
}
|
||||||
if (choice.containsKey("clientLanguage")) {
|
return getDouble(retryPolicy, RETRY_POLICY_MAX_ATTEMPTS_KEY).intValue();
|
||||||
List<Object> clientLanguages = getList(choice, "clientLanguage");
|
|
||||||
if (!clientLanguages.isEmpty()) {
|
|
||||||
boolean javaPresent = false;
|
|
||||||
for (int i = 0; i < clientLanguages.size(); i++) {
|
|
||||||
String lang = getString(clientLanguages, i).toLowerCase(Locale.ROOT);
|
|
||||||
if ("java".equals(lang)) {
|
|
||||||
javaPresent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!javaPresent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (choice.containsKey("percentage")) {
|
|
||||||
int pct = getDouble(choice, "percentage").intValue();
|
|
||||||
Verify.verify(pct >= 0 && pct <= 100, "Bad percentage", choice.get("percentage"));
|
|
||||||
if (random.nextInt(100) >= pct) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (choice.containsKey("clientHostname")) {
|
|
||||||
List<Object> clientHostnames = getList(choice, "clientHostname");
|
|
||||||
if (!clientHostnames.isEmpty()) {
|
|
||||||
boolean hostnamePresent = false;
|
|
||||||
for (int i = 0; i < clientHostnames.size(); i++) {
|
|
||||||
if (getString(clientHostnames, i).equals(hostname)) {
|
|
||||||
hostnamePresent = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hostnamePresent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getObject(choice, "serviceConfig");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@Nullable
|
||||||
static List<Map<String, Object>> parseTxtResults(List<String> txtRecords) {
|
static Long getInitialBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy) {
|
||||||
List<Map<String, Object>> serviceConfigs = new ArrayList<Map<String, Object>>();
|
if (!retryPolicy.containsKey(RETRY_POLICY_INITIAL_BACKOFF_KEY)) {
|
||||||
|
return null;
|
||||||
for (String txtRecord : txtRecords) {
|
|
||||||
if (txtRecord.startsWith(SERVICE_CONFIG_PREFIX)) {
|
|
||||||
List<Map<String, Object>> choices;
|
|
||||||
try {
|
|
||||||
Object rawChoices = JsonParser.parse(txtRecord.substring(SERVICE_CONFIG_PREFIX.length()));
|
|
||||||
if (!(rawChoices instanceof List)) {
|
|
||||||
throw new IOException("wrong type" + rawChoices);
|
|
||||||
}
|
|
||||||
List<Object> listChoices = (List<Object>) rawChoices;
|
|
||||||
for (Object obj : listChoices) {
|
|
||||||
if (!(obj instanceof Map)) {
|
|
||||||
throw new IOException("wrong element type" + rawChoices);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
choices = (List<Map<String, Object>>) (List) listChoices;
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.log(Level.WARNING, "Bad service config: " + txtRecord, e);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
serviceConfigs.addAll(choices);
|
|
||||||
} else {
|
|
||||||
logger.log(Level.FINE, "Ignoring non service config {0}", new Object[]{txtRecord});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return serviceConfigs;
|
String rawInitialBackoff = getString(retryPolicy, RETRY_POLICY_INITIAL_BACKOFF_KEY);
|
||||||
|
try {
|
||||||
|
return parseDuration(rawInitialBackoff);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Long getMaxBackoffNanosFromRetryPolicy(Map<String, Object> retryPolicy) {
|
||||||
|
if (!retryPolicy.containsKey(RETRY_POLICY_MAX_BACKOFF_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String rawMaxBackoff = getString(retryPolicy, RETRY_POLICY_MAX_BACKOFF_KEY);
|
||||||
|
try {
|
||||||
|
return parseDuration(rawMaxBackoff);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Double getBackoffMultiplierFromRetryPolicy(Map<String, Object> retryPolicy) {
|
||||||
|
if (!retryPolicy.containsKey(RETRY_POLICY_BACKOFF_MULTIPLIER_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getDouble(retryPolicy, RETRY_POLICY_BACKOFF_MULTIPLIER_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static List<String> getRetryableStatusCodesFromRetryPolicy(Map<String, Object> retryPolicy) {
|
||||||
|
if (!retryPolicy.containsKey(RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return checkStringList(getList(retryPolicy, RETRY_POLICY_RETRYABLE_STATUS_CODES_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static String getServiceFromName(Map<String, Object> name) {
|
||||||
|
if (!name.containsKey(NAME_SERVICE_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getString(name, NAME_SERVICE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static String getMethodFromName(Map<String, Object> name) {
|
||||||
|
if (!name.containsKey(NAME_METHOD_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getString(name, NAME_METHOD_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Map<String, Object> getRetryPolicyFromMethodConfig(Map<String, Object> methodConfig) {
|
||||||
|
if (!methodConfig.containsKey(METHOD_CONFIG_RETRY_POLICY_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getObject(methodConfig, METHOD_CONFIG_RETRY_POLICY_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static List<Map<String, Object>> getNameListFromMethodConfig(Map<String, Object> methodConfig) {
|
||||||
|
if (!methodConfig.containsKey(METHOD_CONFIG_NAME_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return checkObjectList(getList(methodConfig, METHOD_CONFIG_NAME_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of nanoseconds of timeout for the given method config.
|
||||||
|
*
|
||||||
|
* @return duration nanoseconds, or {@code null} if it isn't present.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
static Long getTimeoutFromMethodConfig(Map<String, Object> methodConfig) {
|
||||||
|
if (!methodConfig.containsKey(METHOD_CONFIG_TIMEOUT_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String rawTimeout = getString(methodConfig, METHOD_CONFIG_TIMEOUT_KEY);
|
||||||
|
try {
|
||||||
|
return parseDuration(rawTimeout);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Boolean getWaitForReadyFromMethodConfig(Map<String, Object> methodConfig) {
|
||||||
|
if (!methodConfig.containsKey(METHOD_CONFIG_WAIT_FOR_READY_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getBoolean(methodConfig, METHOD_CONFIG_WAIT_FOR_READY_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Integer getMaxRequestMessageBytesFromMethodConfig(Map<String, Object> methodConfig) {
|
||||||
|
if (!methodConfig.containsKey(METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getDouble(methodConfig, METHOD_CONFIG_MAX_REQUEST_MESSAGE_BYTES_KEY).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Integer getMaxResponseMessageBytesFromMethodConfig(Map<String, Object> methodConfig) {
|
||||||
|
if (!methodConfig.containsKey(METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getDouble(methodConfig, METHOD_CONFIG_MAX_RESPONSE_MESSAGE_BYTES_KEY).intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static List<Map<String, Object>> getMethodConfigFromServiceConfig(
|
||||||
|
Map<String, Object> serviceConfig) {
|
||||||
|
if (!serviceConfig.containsKey(SERVICE_CONFIG_METHOD_CONFIG_KEY)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return checkObjectList(getList(serviceConfig, SERVICE_CONFIG_METHOD_CONFIG_KEY));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static String getLoadBalancingPolicyFromServiceConfig(Map<String, Object> serviceConfig) {
|
||||||
|
if (!serviceConfig.containsKey(SERVICE_CONFIG_LOAD_BALANCING_POLICY_key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getString(serviceConfig, SERVICE_CONFIG_LOAD_BALANCING_POLICY_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a list from an object for the given key.
|
* Gets a list from an object for the given key.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static List<Object> getList(Map<String, Object> obj, String key) {
|
static List<Object> getList(Map<String, Object> obj, String key) {
|
||||||
assert obj.containsKey(key);
|
assert obj.containsKey(key);
|
||||||
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
||||||
if (value instanceof List) {
|
if (value instanceof List) {
|
||||||
|
|
@ -309,7 +362,7 @@ final class ServiceConfigUtil {
|
||||||
* Gets an object from an object for the given key.
|
* Gets an object from an object for the given key.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static Map<String, Object> getObject(Map<String, Object> obj, String key) {
|
static Map<String, Object> getObject(Map<String, Object> obj, String key) {
|
||||||
assert obj.containsKey(key);
|
assert obj.containsKey(key);
|
||||||
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
||||||
if (value instanceof Map) {
|
if (value instanceof Map) {
|
||||||
|
|
@ -337,7 +390,7 @@ final class ServiceConfigUtil {
|
||||||
* Gets a double from an object for the given key.
|
* Gets a double from an object for the given key.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static Double getDouble(Map<String, Object> obj, String key) {
|
static Double getDouble(Map<String, Object> obj, String key) {
|
||||||
assert obj.containsKey(key);
|
assert obj.containsKey(key);
|
||||||
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
||||||
if (value instanceof Double) {
|
if (value instanceof Double) {
|
||||||
|
|
@ -351,7 +404,7 @@ final class ServiceConfigUtil {
|
||||||
* Gets a string from an object for the given key.
|
* Gets a string from an object for the given key.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static String getString(Map<String, Object> obj, String key) {
|
static String getString(Map<String, Object> obj, String key) {
|
||||||
assert obj.containsKey(key);
|
assert obj.containsKey(key);
|
||||||
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
||||||
if (value instanceof String) {
|
if (value instanceof String) {
|
||||||
|
|
@ -365,7 +418,7 @@ final class ServiceConfigUtil {
|
||||||
* Gets a string from an object for the given index.
|
* Gets a string from an object for the given index.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private static String getString(List<Object> list, int i) {
|
static String getString(List<Object> list, int i) {
|
||||||
assert i >= 0 && i < list.size();
|
assert i >= 0 && i < list.size();
|
||||||
Object value = checkNotNull(list.get(i), "idx %s in %s is null", i, list);
|
Object value = checkNotNull(list.get(i), "idx %s in %s is null", i, list);
|
||||||
if (value instanceof String) {
|
if (value instanceof String) {
|
||||||
|
|
@ -374,4 +427,170 @@ final class ServiceConfigUtil {
|
||||||
throw new ClassCastException(
|
throw new ClassCastException(
|
||||||
String.format("value %s for idx %d in %s is not String", value, i, list));
|
String.format("value %s for idx %d in %s is not String", value, i, list));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a boolean from an object for the given key.
|
||||||
|
*/
|
||||||
|
static Boolean getBoolean(Map<String, Object> obj, String key) {
|
||||||
|
assert obj.containsKey(key);
|
||||||
|
Object value = checkNotNull(obj.get(key), "no such key %s", key);
|
||||||
|
if (value instanceof Boolean) {
|
||||||
|
return (Boolean) value;
|
||||||
|
}
|
||||||
|
throw new ClassCastException(
|
||||||
|
String.format("value %s for key %s in %s is not Boolean", value, key, obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static List<Map<String, Object>> checkObjectList(List<Object> rawList) {
|
||||||
|
for (int i = 0; i < rawList.size(); i++) {
|
||||||
|
if (!(rawList.get(i) instanceof Map)) {
|
||||||
|
throw new ClassCastException(
|
||||||
|
String.format("value %s for idx %d in %s is not object", rawList.get(i), i, rawList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (List<Map<String, Object>>) (List<?>) rawList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
static List<String> checkStringList(List<Object> rawList) {
|
||||||
|
for (int i = 0; i < rawList.size(); i++) {
|
||||||
|
if (!(rawList.get(i) instanceof String)) {
|
||||||
|
throw new ClassCastException(
|
||||||
|
String.format("value %s for idx %d in %s is not string", rawList.get(i), i, rawList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (List<String>) (List<?>) rawList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse from a string to produce a duration. Copy of
|
||||||
|
* {@link com.google.protobuf.util.Durations#parse}.
|
||||||
|
*
|
||||||
|
* @return A Duration parsed from the string.
|
||||||
|
* @throws ParseException if parsing fails.
|
||||||
|
*/
|
||||||
|
private static long parseDuration(String value) throws ParseException {
|
||||||
|
// Must ended with "s".
|
||||||
|
if (value.isEmpty() || value.charAt(value.length() - 1) != 's') {
|
||||||
|
throw new ParseException("Invalid duration string: " + value, 0);
|
||||||
|
}
|
||||||
|
boolean negative = false;
|
||||||
|
if (value.charAt(0) == '-') {
|
||||||
|
negative = true;
|
||||||
|
value = value.substring(1);
|
||||||
|
}
|
||||||
|
String secondValue = value.substring(0, value.length() - 1);
|
||||||
|
String nanoValue = "";
|
||||||
|
int pointPosition = secondValue.indexOf('.');
|
||||||
|
if (pointPosition != -1) {
|
||||||
|
nanoValue = secondValue.substring(pointPosition + 1);
|
||||||
|
secondValue = secondValue.substring(0, pointPosition);
|
||||||
|
}
|
||||||
|
long seconds = Long.parseLong(secondValue);
|
||||||
|
int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue);
|
||||||
|
if (seconds < 0) {
|
||||||
|
throw new ParseException("Invalid duration string: " + value, 0);
|
||||||
|
}
|
||||||
|
if (negative) {
|
||||||
|
seconds = -seconds;
|
||||||
|
nanos = -nanos;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return normalizedDuration(seconds, nanos);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ParseException("Duration value is out of range.", 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy of {@link com.google.protobuf.util.Timestamps#parseNanos}.
|
||||||
|
*/
|
||||||
|
private static int parseNanos(String value) throws ParseException {
|
||||||
|
int result = 0;
|
||||||
|
for (int i = 0; i < 9; ++i) {
|
||||||
|
result = result * 10;
|
||||||
|
if (i < value.length()) {
|
||||||
|
if (value.charAt(i) < '0' || value.charAt(i) > '9') {
|
||||||
|
throw new ParseException("Invalid nanoseconds.", 0);
|
||||||
|
}
|
||||||
|
result += value.charAt(i) - '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("NarrowingCompoundAssignment")
|
||||||
|
private static long normalizedDuration(long seconds, int nanos) {
|
||||||
|
if (nanos <= -NANOS_PER_SECOND || nanos >= NANOS_PER_SECOND) {
|
||||||
|
seconds = checkedAdd(seconds, nanos / NANOS_PER_SECOND);
|
||||||
|
nanos %= NANOS_PER_SECOND;
|
||||||
|
}
|
||||||
|
if (seconds > 0 && nanos < 0) {
|
||||||
|
nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding)
|
||||||
|
seconds--; // no overflow since seconds is positive (and we're decrementing)
|
||||||
|
}
|
||||||
|
if (seconds < 0 && nanos > 0) {
|
||||||
|
nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting)
|
||||||
|
seconds++; // no overflow since seconds is negative (and we're incrementing)
|
||||||
|
}
|
||||||
|
if (!durationIsValid(seconds, nanos)) {
|
||||||
|
throw new IllegalArgumentException(String.format(
|
||||||
|
"Duration is not valid. See proto definition for valid values. "
|
||||||
|
+ "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. "
|
||||||
|
+ "Nanos (%s) must be in range [-999,999,999, +999,999,999]. "
|
||||||
|
+ "Nanos must have the same sign as seconds", seconds, nanos));
|
||||||
|
}
|
||||||
|
return saturatedAdd(TimeUnit.SECONDS.toNanos(seconds), nanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given number of seconds and nanos is a valid {@code Duration}. The {@code
|
||||||
|
* seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos}
|
||||||
|
* value must be in the range [-999,999,999, +999,999,999].
|
||||||
|
*
|
||||||
|
* <p><b>Note:</b> Durations less than one second are represented with a 0 {@code seconds} field
|
||||||
|
* and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero
|
||||||
|
* value for the {@code nanos} field must be of the same sign as the {@code seconds} field.
|
||||||
|
*
|
||||||
|
* <p>Copy of {@link com.google.protobuf.util.Duration#isValid}.</p>
|
||||||
|
*/
|
||||||
|
private static boolean durationIsValid(long seconds, int nanos) {
|
||||||
|
if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seconds < 0 || nanos < 0) {
|
||||||
|
if (seconds > 0 || nanos > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the sum of {@code a} and {@code b} unless it would overflow or underflow in which case
|
||||||
|
* {@code Long.MAX_VALUE} or {@code Long.MIN_VALUE} is returned, respectively.
|
||||||
|
*
|
||||||
|
* <p>Copy of {@link com.google.common.math.LongMath#saturatedAdd}.</p>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("ShortCircuitBoolean")
|
||||||
|
private static long saturatedAdd(long a, long b) {
|
||||||
|
long naiveSum = a + b;
|
||||||
|
if ((a ^ b) < 0 | (a ^ naiveSum) >= 0) {
|
||||||
|
// If a and b have different signs or a has the same sign as the result then there was no
|
||||||
|
// overflow, return.
|
||||||
|
return naiveSum;
|
||||||
|
}
|
||||||
|
// we did over/under flow, if the sign is negative we should return MAX otherwise MIN
|
||||||
|
return Long.MAX_VALUE + ((naiveSum >>> (Long.SIZE - 1)) ^ 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.grpc.internal;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
|
@ -45,15 +46,19 @@ import java.net.UnknownHostException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
|
import java.util.Random;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Assume;
|
import org.junit.Assume;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
import org.junit.rules.Timeout;
|
import org.junit.rules.Timeout;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.JUnit4;
|
import org.junit.runners.JUnit4;
|
||||||
|
|
@ -68,6 +73,11 @@ public class DnsNameResolverTest {
|
||||||
|
|
||||||
@Rule public final Timeout globalTimeout = Timeout.seconds(10);
|
@Rule public final Timeout globalTimeout = Timeout.seconds(10);
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public final ExpectedException thrown = ExpectedException.none();
|
||||||
|
|
||||||
|
private final Map<String, Object> serviceConfig = new LinkedHashMap<String, Object>();
|
||||||
|
|
||||||
private static final int DEFAULT_PORT = 887;
|
private static final int DEFAULT_PORT = 887;
|
||||||
private static final Attributes NAME_RESOLVER_PARAMS =
|
private static final Attributes NAME_RESOLVER_PARAMS =
|
||||||
Attributes.newBuilder().set(NameResolver.Factory.PARAMS_DEFAULT_PORT, DEFAULT_PORT).build();
|
Attributes.newBuilder().set(NameResolver.Factory.PARAMS_DEFAULT_PORT, DEFAULT_PORT).build();
|
||||||
|
|
@ -281,6 +291,239 @@ public class DnsNameResolverTest {
|
||||||
assertEquals("blah\\blah", DnsNameResolver.unquote("\"blah\\\\blah\""));
|
assertEquals("blah\\blah", DnsNameResolver.unquote("\"blah\\\\blah\""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_failsOnMisspelling() {
|
||||||
|
Map<String, Object> bad = new LinkedHashMap<String, Object>();
|
||||||
|
bad.put("parcentage", 1.0);
|
||||||
|
thrown.expectMessage("Bad key");
|
||||||
|
|
||||||
|
DnsNameResolver.maybeChooseServiceConfig(bad, new Random(), "host");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_clientLanguageMatchesJava() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> langs = new ArrayList<Object>();
|
||||||
|
langs.add("java");
|
||||||
|
choice.put("clientLanguage", langs);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_clientLanguageDoesntMatchGo() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> langs = new ArrayList<Object>();
|
||||||
|
langs.add("go");
|
||||||
|
choice.put("clientLanguage", langs);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_clientLanguageCaseInsensitive() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> langs = new ArrayList<Object>();
|
||||||
|
langs.add("JAVA");
|
||||||
|
choice.put("clientLanguage", langs);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_clientLanguageMatchesEmtpy() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> langs = new ArrayList<Object>();
|
||||||
|
choice.put("clientLanguage", langs);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_clientLanguageMatchesMulti() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> langs = new ArrayList<Object>();
|
||||||
|
langs.add("go");
|
||||||
|
langs.add("java");
|
||||||
|
choice.put("clientLanguage", langs);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageZeroAlwaysFails() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 0D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageHundredAlwaysSucceeds() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 100D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageAboveMatches50() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 50D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
Random r = new Random() {
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return 49;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageAtFails50() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 50D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
Random r = new Random() {
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageAboveMatches99() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 99D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
Random r = new Random() {
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return 98;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageAtFails99() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 99D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
Random r = new Random() {
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return 99;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageAboveMatches1() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 1D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
Random r = new Random() {
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_percentageAtFails1() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
choice.put("percentage", 1D);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
Random r = new Random() {
|
||||||
|
@Override
|
||||||
|
public int nextInt(int bound) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, r, "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_hostnameMatches() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> hosts = new ArrayList<Object>();
|
||||||
|
hosts.add("localhost");
|
||||||
|
choice.put("clientHostname", hosts);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_hostnameDoesntMatch() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> hosts = new ArrayList<Object>();
|
||||||
|
hosts.add("localhorse");
|
||||||
|
choice.put("clientHostname", hosts);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_clientLanguageCaseSensitive() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> hosts = new ArrayList<Object>();
|
||||||
|
hosts.add("LOCALHOST");
|
||||||
|
choice.put("clientHostname", hosts);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_hostnameMatchesEmtpy() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> hosts = new ArrayList<Object>();
|
||||||
|
choice.put("clientHostname", hosts);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void maybeChooseServiceConfig_hostnameMatchesMulti() {
|
||||||
|
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
||||||
|
List<Object> hosts = new ArrayList<Object>();
|
||||||
|
hosts.add("localhorse");
|
||||||
|
hosts.add("localhost");
|
||||||
|
choice.put("clientHostname", hosts);
|
||||||
|
choice.put("serviceConfig", serviceConfig);
|
||||||
|
|
||||||
|
assertNotNull(DnsNameResolver.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
||||||
|
}
|
||||||
|
|
||||||
private void testInvalidUri(URI uri) {
|
private void testInvalidUri(URI uri) {
|
||||||
try {
|
try {
|
||||||
provider.newNameResolver(uri, NAME_RESOLVER_PARAMS);
|
provider.newNameResolver(uri, NAME_RESOLVER_PARAMS);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,415 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018, 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.mockito.Matchers.eq;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import io.grpc.CallOptions;
|
||||||
|
import io.grpc.Channel;
|
||||||
|
import io.grpc.Deadline;
|
||||||
|
import io.grpc.MethodDescriptor;
|
||||||
|
import io.grpc.MethodDescriptor.MethodType;
|
||||||
|
import io.grpc.internal.ServiceConfigInterceptor.MethodInfo;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.ExpectedException;
|
||||||
|
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 ServiceConfigInterceptor}.
|
||||||
|
*/
|
||||||
|
@RunWith(JUnit4.class)
|
||||||
|
public class ServiceConfigInterceptorTest {
|
||||||
|
|
||||||
|
@Rule public final ExpectedException thrown = ExpectedException.none();
|
||||||
|
|
||||||
|
@Mock private Channel channel;
|
||||||
|
@Captor private ArgumentCaptor<CallOptions> callOptionsCap;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ServiceConfigInterceptor interceptor = new ServiceConfigInterceptor();
|
||||||
|
|
||||||
|
private final String fullMethodName =
|
||||||
|
MethodDescriptor.generateFullMethodName("service", "method");
|
||||||
|
private final MethodDescriptor<Void, Void> methodDescriptor =
|
||||||
|
MethodDescriptor.newBuilder(new NoopMarshaller(), new NoopMarshaller())
|
||||||
|
.setType(MethodType.UNARY)
|
||||||
|
.setFullMethodName(fullMethodName)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private static final class JsonObj extends HashMap<String, Object> {
|
||||||
|
private JsonObj(Object ... kv) {
|
||||||
|
for (int i = 0; i < kv.length; i += 2) {
|
||||||
|
put((String) kv[i], kv[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class JsonList extends ArrayList<Object> {
|
||||||
|
private JsonList(Object ... values) {
|
||||||
|
addAll(Arrays.asList(values));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withWaitForReady() {
|
||||||
|
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.interceptCall(methodDescriptor, CallOptions.DEFAULT.withoutWaitForReady(), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().isWaitForReady()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withMaxRequestSize() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", 1d);
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withMaxRequestSize_pickSmallerExisting() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", 10d);
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(
|
||||||
|
methodDescriptor, CallOptions.DEFAULT.withMaxOutboundMessageSize(5), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withMaxRequestSize_pickSmallerNew() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", 5d);
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(
|
||||||
|
methodDescriptor, CallOptions.DEFAULT.withMaxOutboundMessageSize(10), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getMaxOutboundMessageSize()).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withMaxResponseSize() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", 1d);
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getMaxInboundMessageSize()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withMaxResponseSize_pickSmallerExisting() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", 5d);
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(
|
||||||
|
methodDescriptor, CallOptions.DEFAULT.withMaxInboundMessageSize(10), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getMaxInboundMessageSize()).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withMaxResponseSize_pickSmallerNew() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", 10d);
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(
|
||||||
|
methodDescriptor, CallOptions.DEFAULT.withMaxInboundMessageSize(5), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getMaxInboundMessageSize()).isEqualTo(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void withoutWaitForReady() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "waitForReady", false);
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT.withWaitForReady(), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().isWaitForReady()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fullMethodMatched() {
|
||||||
|
// Put in service that matches, but has no deadline. It should be lower priority
|
||||||
|
JsonObj name1 = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig1 = new JsonObj("name", new JsonList(name1));
|
||||||
|
|
||||||
|
JsonObj name2 = new JsonObj("service", "service", "method", "method");
|
||||||
|
JsonObj methodConfig2 = new JsonObj("name", new JsonList(name2), "timeout", "1s");
|
||||||
|
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig1, methodConfig2));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
interceptor.interceptCall(methodDescriptor, CallOptions.DEFAULT, channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getDeadline()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nearerDeadlineKept_existing() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "100000s");
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
Deadline existingDeadline = Deadline.after(1000, TimeUnit.NANOSECONDS);
|
||||||
|
interceptor.interceptCall(
|
||||||
|
methodDescriptor, CallOptions.DEFAULT.withDeadline(existingDeadline), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getDeadline()).isEqualTo(existingDeadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nearerDeadlineKept_new() {
|
||||||
|
// TODO(carl-mastrangelo): the deadlines are very large because they change over time.
|
||||||
|
// This should be fixed, and is tracked in https://github.com/grpc/grpc-java/issues/2531
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "1s");
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
Deadline existingDeadline = Deadline.after(1234567890, TimeUnit.NANOSECONDS);
|
||||||
|
interceptor.interceptCall(
|
||||||
|
methodDescriptor, CallOptions.DEFAULT.withDeadline(existingDeadline), channel);
|
||||||
|
|
||||||
|
verify(channel).newCall(eq(methodDescriptor), callOptionsCap.capture());
|
||||||
|
assertThat(callOptionsCap.getValue().getDeadline()).isNotEqualTo(existingDeadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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");
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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");
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void handleUpdate_failsOnEmptyName() {
|
||||||
|
JsonObj methodConfig = new JsonObj();
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
thrown.expect(IllegalArgumentException.class);
|
||||||
|
thrown.expectMessage("no names in method config");
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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));
|
||||||
|
JsonObj serviceConfig = new JsonObj("methodConfig", new JsonList(methodConfig));
|
||||||
|
|
||||||
|
thrown.expect(IllegalArgumentException.class);
|
||||||
|
thrown.expectMessage("Duplicate service");
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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");
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void handleUpdate_replaceExistingConfig() {
|
||||||
|
JsonObj name1 = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig1 = new JsonObj("name", new JsonList(name1));
|
||||||
|
JsonObj serviceConfig1 = new JsonObj("methodConfig", new JsonList(methodConfig1));
|
||||||
|
|
||||||
|
JsonObj name2 = new JsonObj("service", "service", "method", "method");
|
||||||
|
JsonObj methodConfig2 = new JsonObj("name", new JsonList(name2));
|
||||||
|
JsonObj serviceConfig2 = new JsonObj("methodConfig", new JsonList(methodConfig2));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig1);
|
||||||
|
|
||||||
|
assertThat(interceptor.serviceMap.get()).isNotEmpty();
|
||||||
|
assertThat(interceptor.serviceMethodMap.get()).isEmpty();
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig2);
|
||||||
|
|
||||||
|
assertThat(interceptor.serviceMap.get()).isEmpty();
|
||||||
|
assertThat(interceptor.serviceMethodMap.get()).isNotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void handleUpdate_matchNames() {
|
||||||
|
JsonObj name1 = new JsonObj("service", "service2");
|
||||||
|
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));
|
||||||
|
|
||||||
|
interceptor.handleUpdate(serviceConfig);
|
||||||
|
|
||||||
|
assertThat(interceptor.serviceMethodMap.get())
|
||||||
|
.containsExactly(
|
||||||
|
methodDescriptor.getFullMethodName(),
|
||||||
|
new MethodInfo(methodConfig));
|
||||||
|
assertThat(interceptor.serviceMap.get())
|
||||||
|
.containsExactly("service2", new MethodInfo(methodConfig));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void methodInfo_validateDeadline() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "10000000000000000s");
|
||||||
|
|
||||||
|
thrown.expectMessage("Duration value is out of range");
|
||||||
|
|
||||||
|
new MethodInfo(methodConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void methodInfo_saturateDeadline() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "timeout", "315576000000s");
|
||||||
|
|
||||||
|
MethodInfo info = new MethodInfo(methodConfig);
|
||||||
|
|
||||||
|
assertThat(info.timeoutNanos).isEqualTo(Long.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void methodInfo_badMaxRequestSize() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxRequestMessageBytes", -1d);
|
||||||
|
|
||||||
|
thrown.expect(IllegalArgumentException.class);
|
||||||
|
thrown.expectMessage("exceeds bounds");
|
||||||
|
|
||||||
|
new MethodInfo(methodConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void methodInfo_badMaxResponseSize() {
|
||||||
|
JsonObj name = new JsonObj("service", "service");
|
||||||
|
JsonObj methodConfig = new JsonObj("name", new JsonList(name), "maxResponseMessageBytes", -1d);
|
||||||
|
|
||||||
|
thrown.expect(IllegalArgumentException.class);
|
||||||
|
thrown.expectMessage("exceeds bounds");
|
||||||
|
|
||||||
|
new MethodInfo(methodConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class NoopMarshaller implements MethodDescriptor.Marshaller<Void> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream stream(Void value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Void parse(InputStream stream) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,276 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2018, 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 org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Random;
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.JUnit4;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for {@link ServiceConfigUtil}.
|
|
||||||
*/
|
|
||||||
@RunWith(JUnit4.class)
|
|
||||||
public class ServiceConfigUtilTest {
|
|
||||||
|
|
||||||
@Rule
|
|
||||||
public final ExpectedException thrown = ExpectedException.none();
|
|
||||||
|
|
||||||
private final Map<String, Object> serviceConfig = new LinkedHashMap<String, Object>();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_failsOnMisspelling() {
|
|
||||||
Map<String, Object> bad = new LinkedHashMap<String, Object>();
|
|
||||||
bad.put("parcentage", 1.0);
|
|
||||||
thrown.expectMessage("Bad key");
|
|
||||||
|
|
||||||
ServiceConfigUtil.maybeChooseServiceConfig(bad, new Random(), "host");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_clientLanguageMatchesJava() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> langs = new ArrayList<Object>();
|
|
||||||
langs.add("java");
|
|
||||||
choice.put("clientLanguage", langs);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_clientLanguageDoesntMatchGo() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> langs = new ArrayList<Object>();
|
|
||||||
langs.add("go");
|
|
||||||
choice.put("clientLanguage", langs);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_clientLanguageCaseInsensitive() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> langs = new ArrayList<Object>();
|
|
||||||
langs.add("JAVA");
|
|
||||||
choice.put("clientLanguage", langs);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_clientLanguageMatchesEmtpy() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> langs = new ArrayList<Object>();
|
|
||||||
choice.put("clientLanguage", langs);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_clientLanguageMatchesMulti() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> langs = new ArrayList<Object>();
|
|
||||||
langs.add("go");
|
|
||||||
langs.add("java");
|
|
||||||
choice.put("clientLanguage", langs);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageZeroAlwaysFails() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 0D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageHundredAlwaysSucceeds() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 100D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageAboveMatches50() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 50D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
Random r = new Random() {
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 49;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageAtFails50() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 50D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
Random r = new Random() {
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 50;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageAboveMatches99() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 99D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
Random r = new Random() {
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 98;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageAtFails99() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 99D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
Random r = new Random() {
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 99;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageAboveMatches1() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 1D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
Random r = new Random() {
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_percentageAtFails1() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
choice.put("percentage", 1D);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
Random r = new Random() {
|
|
||||||
@Override
|
|
||||||
public int nextInt(int bound) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, r, "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_hostnameMatches() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> hosts = new ArrayList<Object>();
|
|
||||||
hosts.add("localhost");
|
|
||||||
choice.put("clientHostname", hosts);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_hostnameDoesntMatch() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> hosts = new ArrayList<Object>();
|
|
||||||
hosts.add("localhorse");
|
|
||||||
choice.put("clientHostname", hosts);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_clientLanguageCaseSensitive() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> hosts = new ArrayList<Object>();
|
|
||||||
hosts.add("LOCALHOST");
|
|
||||||
choice.put("clientHostname", hosts);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_hostnameMatchesEmtpy() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> hosts = new ArrayList<Object>();
|
|
||||||
choice.put("clientHostname", hosts);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "host"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void maybeChooseServiceConfig_hostnameMatchesMulti() {
|
|
||||||
Map<String, Object> choice = new LinkedHashMap<String, Object>();
|
|
||||||
List<Object> hosts = new ArrayList<Object>();
|
|
||||||
hosts.add("localhorse");
|
|
||||||
hosts.add("localhost");
|
|
||||||
choice.put("clientHostname", hosts);
|
|
||||||
choice.put("serviceConfig", serviceConfig);
|
|
||||||
|
|
||||||
assertNotNull(ServiceConfigUtil.maybeChooseServiceConfig(choice, new Random(), "localhost"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue